diff options
| -rw-r--r-- | CONTRIBUTING.md | 48 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 45 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 57 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 7 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 37 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 4 | ||||
| -rw-r--r-- | pages/popup.coffee | 16 | ||||
| -rw-r--r-- | tests/unit_tests/completion_test.coffee | 34 |
9 files changed, 156 insertions, 104 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9382a020..a417caf5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,34 @@ reports: When you're done with your changes, send us a pull request on Github. Feel free to include a change to the CREDITS file with your patch. +Vimium design goals +------------------- + +When improving Vimium it's helpful to know what design goals we're optimizing for. + +The core goal is to make it easy to navigate the web using just the keyboard. When people first start using +Vimium, it provides an incredibly powerful workflow improvement and it makes them feel awesome. And it turns +out that Vimium is applicable to a huge, broad population of people, not just users of Vim, which is great. + +A secondary goal is to make Vimium approachable, or in other words, to minimize the barriers which will +prevent a new user from feeling awesome. Many of Vimium's users haven't used Vim before (about 1 in 5 app +store reviews say this), and most people have strong web browsing habits forged from years of browsing that +they rely on. Given that, it's a great experience when Vimium feels like a natural addition to Chrome which +augments but doesn't break their current browsing habits. + +In some ways, making software approachable is even harder than just enabling the core use case. But in this +area, Vimium really shines. It's approachable today because: + +1. It's simple to understand (even if you're not very familiar with Vim). The Vimium video shows you all you + need to know to start using Vimium and feel awesome. +2. The core feature set works in almost all cases on all sites, so Vimium feels reliable. +3. Requires no configuration or doc-reading before it's useful. Just watch the video or hit `?`. +4. Doesn't drastically change the way Chrome looks or behaves. You can transition into using Vimium piecemeal; + you don't need to jump in whole-hog from the start. +5. The core feature set isn't overwhelming. This is easy to degrade as we evolve Vimium, so it requires active + effort to maintain this feel. +6. Developers find the code is relatively simple and easy to jump into, so we have an active dev community. + ## What makes for a good feature request/contribution to Vimium? Good features: @@ -96,23 +124,3 @@ We use these guidelines, in addition to the code complexity, when deciding wheth If you're worried that a feature you plan to build won't be a good fit for core Vimium, just open a github issue for discussion or send an email to the Vimium mailing list. - -## How to release Vimium to the Chrome Store - -This process is currently only done by Phil or Ilya. - -1. Increment the version number in manifest.json -2. Update the Changelog in README.md - - You can see a summary of commits since the last version: `git log --oneline v1.45..` - -3. Push your commits -4. Create a git tag for this newly released version - - git tag -a v1.45 -m "v1.45 release" - -5. Run `cake package` -6. Take the distributable found in `dist` and upload it - [here](https://chrome.google.com/webstore/developer/dashboard) -7. Update the description in the Chrome store to include the latest version's release notes -8. Celebrate @@ -3,8 +3,8 @@ Vimium - The Hacker's Browser [](https://travis-ci.org/philc/vimium) -Vimium is a Chrome extension that provides keyboard-based navigation and control in the spirit of the Vim -editor. +Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of +the Vim editor. __Installation instructions:__ @@ -15,14 +15,16 @@ Please see [CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md#installing-from-source) for instructions on how you can install Vimium from source. -The Options page can be reached via a link on the help dialog (hit `?`) or via the button next to Vimium on +The Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on the Chrome Extensions page (`chrome://extensions`). Keyboard Bindings ----------------- Modifier keys are specified as `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x -respectively. See the next section for instructions on customizing these bindings. +respectively. See the next section for how to customize these bindings. + +Once you have Vimium installed, you can see this list of key bindings at any time by typing `?`. Navigating the current page: @@ -90,7 +92,7 @@ Additional advanced browsing commands: 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 +There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full list. Custom Key Mappings diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index b6a52a15..dc5519d5 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -26,7 +26,6 @@ class Suggestion generateHtml: -> return @html if @html - favIconUrl = @tabFavIconUrl or "#{@getUrlRoot(@url)}/favicon.ico" relevancyHtml = if @showRelevancy then "<span class='relevancy'>#{@computeRelevancy()}</span>" else "" # NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS. @html = @@ -35,8 +34,7 @@ class Suggestion <span class="vimiumReset vomnibarSource">#{@type}</span> <span class="vimiumReset vomnibarTitle">#{@highlightTerms(Utils.escapeHtml(@title))}</span> </div> - <div class="vimiumReset vomnibarBottomHalf vomnibarIcon" - style="background-image: url(#{favIconUrl});"> + <div class="vimiumReset vomnibarBottomHalf"> <span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(Utils.escapeHtml(@url)))}</span> #{relevancyHtml} </div> @@ -261,26 +259,44 @@ class DomainCompleter # Suggestions from the Domain completer have the maximum relevancy. They should be shown first in the list. computeRelevancy: -> 1 -# TabRecency associates a logical timestamp with each tab id. +# TabRecency associates a logical timestamp with each tab id. These are used to provide an initial +# recency-based ordering in the tabs vomnibar (which allows jumping quickly between recently-visited tabs). class TabRecency - constructor: -> - @timestamp = 1 - @cache = {} + timestamp: 1 + current: -1 + cache: {} + lastVisited: null + lastVisitedTime: null + timeDelta: 500 # Milliseconds. - chrome.tabs.onActivated.addListener (activeInfo) => @add activeInfo.tabId - chrome.tabs.onRemoved.addListener (tabId) => @remove tabId + constructor: -> + chrome.tabs.onActivated.addListener (activeInfo) => @register activeInfo.tabId + chrome.tabs.onRemoved.addListener (tabId) => @deregister tabId chrome.tabs.onReplaced.addListener (addedTabId, removedTabId) => - @remove removedTabId - @add addedTabId + @deregister removedTabId + @register addedTabId + + register: (tabId) -> + currentTime = new Date() + # Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a + # very-short time (e.g. those passed through with `5J`) aren't registered as visited at all. + if @lastVisitedTime? and @timeDelta <= currentTime - @lastVisitedTime + @cache[@lastVisited] = ++@timestamp + + @current = @lastVisited = tabId + @lastVisitedTime = currentTime - add: (tabId) -> @cache[tabId] = ++@timestamp - remove: (tabId) -> delete @cache[tabId] + deregister: (tabId) -> + if tabId == @lastVisited + # Ensure we don't register this tab, since it's going away. + @lastVisited = @lastVisitedTime = null + delete @cache[tabId] # Recently-visited tabs get a higher score (except the current tab, which gets a low score). recencyScore: (tabId) -> @cache[tabId] ||= 1 - if @cache[tabId] == @timestamp then 0.0 else @cache[tabId] / @timestamp + if tabId == @current then 0.0 else @cache[tabId] / @timestamp tabRecency = new TabRecency() @@ -294,7 +310,6 @@ class TabCompleter suggestions = results.map (tab) => suggestion = new Suggestion(queryTerms, "tab", tab.url, tab.title, @computeRelevancy) suggestion.tabId = tab.id - suggestion.tabFavIconUrl = tab.favIconUrl suggestion onComplete(suggestions) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index b40907fb..3ec618c9 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -8,7 +8,7 @@ keyQueue = "" # Queue of keys typed validFirstKeys = {} singleKeyCommands = [] focusedFrame = null -framesForTab = {} +frameIdsForTab = {} # 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 @@ -282,16 +282,14 @@ BackgroundCommands = { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId })) moveTabLeft: (count) -> moveTab(null, -count) moveTabRight: (count) -> moveTab(null, count) - nextFrame: (count) -> + nextFrame: (count,frameId) -> chrome.tabs.getSelected(null, (tab) -> - frames = framesForTab[tab.id].frames - currIndex = getCurrFrameIndex(frames) - - # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), - # since it exists only to contain the other frames. - newIndex = (currIndex + count) % frames.length - - chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[newIndex].id, highlight: true })) + frames = frameIdsForTab[tab.id] + # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an + # additional offset such that we do indeed start from frameId. + count = (count + Math.max 0, frameIdsForTab[tab.id].indexOf frameId) % frames.length + frames = frameIdsForTab[tab.id] = [frames[count..]..., frames[0...count]...] + chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true })) closeTabsOnLeft: -> removeTabsRelative "before" closeTabsOnRight: -> removeTabsRelative "after" @@ -347,7 +345,7 @@ updateOpenTabs = (tab) -> scrollY: null deletor: null # Frames are recreated on refresh - delete framesForTab[tab.id] + delete frameIdsForTab[tab.id] setBrowserActionIcon = (tabId,path) -> chrome.browserAction.setIcon({ tabId: tabId, path: path }) @@ -394,7 +392,7 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> code: Settings.get("userDefinedLinkHintCss") runAt: "document_start" chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError - updateOpenTabs(tab) + updateOpenTabs(tab) if changeInfo.url? updateActiveState(tabId) chrome.tabs.onAttached.addListener (tabId, attachedInfo) -> @@ -429,7 +427,7 @@ chrome.tabs.onRemoved.addListener (tabId) -> # scroll position) tabInfoMap.deletor = -> delete tabInfoMap[tabId] setTimeout tabInfoMap.deletor, 1000 - delete framesForTab[tabId] + delete frameIdsForTab[tabId] chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId) @@ -554,9 +552,9 @@ checkKeyQueue = (keysToCheck, tabId, frameId) -> refreshedCompletionKeys = true else if registryEntry.passCountToFunction - BackgroundCommands[registryEntry.command](count) + BackgroundCommands[registryEntry.command](count, frameId) else if registryEntry.noRepeat - BackgroundCommands[registryEntry.command]() + BackgroundCommands[registryEntry.command](frameId) else repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId) @@ -603,21 +601,21 @@ openOptionsPageInNewTab = -> chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 })) registerFrame = (request, sender) -> - unless framesForTab[sender.tab.id] - framesForTab[sender.tab.id] = { frames: [] } - - if (request.is_top) - focusedFrame = request.frameId - framesForTab[sender.tab.id].total = request.total + (frameIdsForTab[sender.tab.id] ?= []).push request.frameId - framesForTab[sender.tab.id].frames.push({ id: request.frameId }) - -handleFrameFocused = (request, sender) -> focusedFrame = request.frameId +unregisterFrame = (request, sender) -> + tabId = sender.tab.id + if frameIdsForTab[tabId]? + if request.tab_is_closing + updateOpenTabs sender.tab + else + frameIdsForTab[tabId] = frameIdsForTab[tabId].filter (id) -> id != request.frameId -getCurrFrameIndex = (frames) -> - for i in [0...frames.length] - return i if frames[i].id == focusedFrame - frames.length + 1 +handleFrameFocused = (request, sender) -> + tabId = sender.tab.id + if frameIdsForTab[tabId]? + frameIdsForTab[tabId] = + [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] # Port handler mapping portHandlers = @@ -633,6 +631,7 @@ sendRequestHandlers = openUrlInCurrentTab: openUrlInCurrentTab, openOptionsPageInNewTab: openOptionsPageInNewTab, registerFrame: registerFrame, + unregisterFrame: unregisterFrame, frameFocused: handleFrameFocused, upgradeNotificationClosed: upgradeNotificationClosed, updateScrollPosition: handleUpdateScrollPosition, @@ -640,7 +639,7 @@ sendRequestHandlers = isEnabledForUrl: isEnabledForUrl, saveHelpDialogSettings: saveHelpDialogSettings, selectSpecificTab: selectSpecificTab, - refreshCompleter: refreshCompleter + refreshCompleter: refreshCompleter, createMark: Marks.create.bind(Marks), gotoMark: Marks.goto.bind(Marks) diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 24f229f3..7998fe5c 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -352,13 +352,6 @@ body.vimiumFindMode ::selection { padding: 2px 0; } -#vomnibar li .vomnibarIcon { - background-position-y: center; - background-size: 16px; - background-repeat: no-repeat; - padding-left: 20px; -} - #vomnibar li .vomnibarSource { color: #777; margin-right: 4px; diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 9c59396f..469afe71 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -179,19 +179,24 @@ window.addEventListener "focus", -> # Initialization tasks that must wait for the document to be ready. # initializeOnDomReady = -> - registerFrame(window.top == window.self) - enterInsertModeIfElementIsFocused() if isEnabledForUrl # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) -registerFrame = (is_top) -> - chrome.runtime.sendMessage( - handler: "registerFrame" +registerFrame = -> + # Don't register frameset containers; focusing them is no use. + if document.body.tagName != "FRAMESET" + chrome.runtime.sendMessage + handler: "registerFrame" + frameId: frameId + +# Unregister the frame if we're going to exit. +unregisterFrame = -> + chrome.runtime.sendMessage + handler: "unregisterFrame" frameId: frameId - is_top: is_top - total: frames.length + 1) + tab_is_closing: window.top == window.self # # Enters insert mode if the currently focused element in the DOM is focusable. @@ -367,7 +372,7 @@ onKeypress = (event) -> else if (!isInsertMode() && !findMode) if (isPassKey keyChar) return undefined - if (currentCompletionKeys.indexOf(keyChar) != -1) + if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -440,7 +445,7 @@ onKeydown = (event) -> else if (!isInsertMode() && !findMode) if (keyChar) - if (currentCompletionKeys.indexOf(keyChar) != -1) + if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent event handledKeydownEvents.push event @@ -502,7 +507,7 @@ refreshCompletionKeys = (response) -> chrome.runtime.sendMessage({ handler: "getCompletionKeys" }, refreshCompletionKeys) isValidFirstKey = (keyChar) -> - validFirstKeys[keyChar] || /[1-9]/.test(keyChar) + validFirstKeys[keyChar] || /^[1-9]/.test(keyChar) onFocusCapturePhase = (event) -> if (isFocusable(event.target) && !findMode) @@ -529,6 +534,7 @@ isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase( # any element which makes it a rich text editor, like the notes on jjot.com. # isEditable = (target) -> + # Note: document.activeElement.isContentEditable is also rechecked in isInsertMode() dynamically. return true if target.isContentEditable nodeName = target.nodeName.toLowerCase() # use a blacklist instead of a whitelist because new form controls are still being implemented for html5 @@ -552,6 +558,7 @@ window.enterInsertMode = (target) -> # when the last editable element that came into focus -- which insertModeLock points to -- has been blurred. # If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only # leave insert mode when the user presses <ESC>. +# Note. This returns the truthiness of target, which is required by isInsertMode. # enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target @@ -560,7 +567,13 @@ exitInsertMode = (target) -> insertModeLock = null HUD.hide() -isInsertMode = -> insertModeLock != null +isInsertMode = -> + return true if insertModeLock != null + # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and + # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check whether + # the active element is contentEditable. + document.activeElement and document.activeElement.isContentEditable and + enterInsertModeWithoutShowingIndicator document.activeElement # should be called whenever rawQuery is modified. updateFindModeQuery = -> @@ -1055,6 +1068,8 @@ Tween = state.onUpdate(value) initializePreDomReady() +window.addEventListener("DOMContentLoaded", registerFrame) +window.addEventListener("unload", unregisterFrame) window.addEventListener("DOMContentLoaded", initializeOnDomReady) window.onbeforeunload = -> diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 62e655e7..21018049 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -32,8 +32,8 @@ DomUtils = # makeXPath: (elementArray) -> xpath = [] - for i of elementArray - xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i]) + for element in elementArray + xpath.push("//" + element, "//xhtml:" + element) xpath.join(" | ") evaluateXPath: (xpath, resultType) -> diff --git a/pages/popup.coffee b/pages/popup.coffee index 2ab97bef..99a4eb87 100644 --- a/pages/popup.coffee +++ b/pages/popup.coffee @@ -3,6 +3,17 @@ originalRule = undefined originalPattern = undefined originalPassKeys = undefined +generateDefaultPattern = (url) -> + if /^https?:\/\/./.test url + # The common use case is to disable Vimium at the domain level. + # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html". + "https?:/" + url.split("/",3)[1..].join("/") + "/*" + else if /^[a-z]{3,}:\/\/./.test url + # Anything else which seems to be a URL. + url.split("/",3).join("/") + "/*" + else + url + "*" + reset = (initialize=false) -> document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html") chrome.tabs.getSelected null, (tab) -> @@ -13,11 +24,8 @@ reset = (initialize=false) -> originalPattern = originalRule.pattern originalPassKeys = originalRule.passKeys else - # The common use case is to disable Vimium at the domain level. - # This regexp will match "http://www.example.com/" from "http://www.example.com/path/to/page.html". - domain = (tab.url.match(/[^\/]*\/\/[^\/]*\//) or tab.url) + "*" originalRule = null - originalPattern = domain + originalPattern = generateDefaultPattern tab.url originalPassKeys = "" patternElement = document.getElementById("popupPattern") passKeysElement = document.getElementById("popupPassKeys") diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee index 88f59b7e..e4966016 100644 --- a/tests/unit_tests/completion_test.coffee +++ b/tests/unit_tests/completion_test.coffee @@ -399,21 +399,32 @@ context "RegexpCache", should "search for a string with a prefix/suffix (negative case)", -> assert.isTrue "hound dog".search(RegexpCache.get("do", "\\b", "\\b")) == -1 +fakeTimeDeltaElapsing = -> + context "TabRecency", setup -> @tabRecency = new TabRecency() - @tabRecency.add 3 - @tabRecency.add 2 - @tabRecency.add 9 - @tabRecency.add 1 - @tabRecency.remove 9 - @tabRecency.add 4 - - should "have entries for active tabs", -> + + fakeTimeDeltaElapsing = => + if @tabRecency.lastVisitedTime? + @tabRecency.lastVisitedTime = new Date(@tabRecency.lastVisitedTime - @tabRecency.timeDelta) + + @tabRecency.register 3 + fakeTimeDeltaElapsing() + @tabRecency.register 2 + fakeTimeDeltaElapsing() + @tabRecency.register 9 + fakeTimeDeltaElapsing() + @tabRecency.register 1 + @tabRecency.deregister 9 + fakeTimeDeltaElapsing() + @tabRecency.register 4 + fakeTimeDeltaElapsing() + + should "have entries for recently active tabs", -> assert.isTrue @tabRecency.cache[1] assert.isTrue @tabRecency.cache[2] assert.isTrue @tabRecency.cache[3] - assert.isTrue @tabRecency.cache[4] should "not have entries for removed tabs", -> assert.isFalse @tabRecency.cache[9] @@ -431,8 +442,9 @@ context "TabRecency", should "rank tabs by recency", -> assert.isTrue @tabRecency.recencyScore(3) < @tabRecency.recencyScore 2 assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 1 - @tabRecency.add 3 - @tabRecency.add 4 # Making 3 the most recent tab which isn't the current tab. + @tabRecency.register 3 + fakeTimeDeltaElapsing() + @tabRecency.register 4 # Making 3 the most recent tab which isn't the current tab. assert.isTrue @tabRecency.recencyScore(1) < @tabRecency.recencyScore 3 assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 3 assert.isTrue @tabRecency.recencyScore(4) < @tabRecency.recencyScore 3 |
