diff --git a/README.md b/README.md index 584656f..59c4345 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,43 @@ [![CircleCI](https://circleci.com/gh/ueokande/vim-vixen.svg?style=svg)](https://circleci.com/gh/ueokande/vim-vixen) [![devDependencies Status](https://david-dm.org/ueokande/vim-vixen/dev-status.svg)](https://david-dm.org/ueokande/vim-vixen?type=dev) -Vim Vixen is a Firefox add-on which allows you to navigate with the keyboard on the browser. -Firefox started to support WebExtensions API and will stop supporting add-ons using legacy APIs from version 57. -For this reason, many legacy add-ons do not work on Firefox 57. -Vim Vixen is a new choice for Vim users since Vim Vixen uses the WebExtensions API. +Vim Vixen is a Firefox add-on which allows you to easily navigate the web by +keyboard. Since version 57, Firefox has migrated to the WebExtensions API and +has dropped support for legacy add-ons. Vim Vixen is a new choice for Vim users +since it uses the WebExtensions API. ## Basic usage -### Key-maps +### Keymaps -The key-maps are configurable in the add-ons preferences by navigating to `about:addons` and selecting "Extensions". +Keymaps are configurable in the add-on's preferences by navigating to `about:addons` and selecting "Extensions". The default mappings are as follows: #### Console -- :: open console -- o, t, w: open a page in current tab, new tab, or new window -- O, T, W: similar to o, t, w, but that contains current URL -- b: Select tabs by URL or title -- a: add current page to the bookmarks +- :: open the console +- o, t, w: open a page in the current tab, a new tab, or new window +- O, T, W: similar to o, t, w, but using the current URL +- b: select tabs by URL or title +- a: add the current page to your bookmarks -See [console commands](#console-commands) section for more detailed description +See the [console commands](#console-commands) section for a more detailed description. #### Tabs -- d: delete tab -- !d: delete pinned tab -- u: reopen close tab -- r: reload current tab -- R: reload current tab without cache -- K or gT: select previous tab -- J or gt: select next tab -- g0: select first tab -- g$: select last tab -- Ctrl+6: open previously selected tab -- zp: pin tab -- zd: duplicate tab + +- d: delete the current tab and select the tab to its right +- D: delete the current tab and select the tab to its left +- !d: delete a pinned tab +- u: reopen a close tab +- r: reload the current tab +- R: reload the current tab, bypassing the cache +- K or gT: select the previous tab +- J or gt: select the next tab +- g0: select the first tab +- g$: select the last tab +- Ctrl+6: open the previously-selected tab +- zp: pin the curent tab tab +- zd: duplicate the current tab #### Scrolling @@ -48,18 +50,19 @@ See [console commands](#console-commands) section for more detailed description - j: scroll down - h: scroll left - l: scroll right -- Ctrl+U: scroll up for a half page -- Ctrl+D: scroll down for a half page -- Ctrl+B: scroll up for a whole page -- Ctrl+F: scroll down for a whole page -- gg: scroll to top of a page -- G: scroll to bottom of a page +- Ctrl+U: scroll up half a page +- Ctrl+D: scroll down half a page +- Ctrl+B: scroll up a page +- Ctrl+F: scroll down a page +- gg: scroll to the top of a page +- G: scroll to the bottom of a page - 0: scroll to the leftmost part of a page - $: scroll to the rightmost part of a page -- m: set a mark from current position -- ': jump to position by the mark +- m: set a mark for the current position +- ': jump to a marked position -Lowercase alphabet mark (`[a-z]`) stores position on the current tab. Upper alphabet and numeric mark (`[A-Z0-9]`) stores position and tab. +Lowercase marks (`[a-z]`) store the position of the current tab. Uppercase and +numeric marks (`[A-Z0-9]`) store the position and the tab. #### Zoom @@ -69,109 +72,111 @@ Lowercase alphabet mark (`[a-z]`) stores position on the current tab. Upper alph #### Navigation -- f: start following links in the page in the current tab -- F: start following links in the page in new tabs +- f: follow links in the page in the current tab +- F: follow links in the page in a new tab - H: go back in history - L: go forward in history -- [[, ]]: find prev or next links and open it -- gu: go to parent directory -- gU: go to root directory -- gi: focus first input - -Plugin can be configured to follow links in new tabs in background instead of -switching to a new tab immediately. To do this you need to update config file: -change `"background"` property of `"follow.start"` action to true: +- [[, ]]: find a link to the previous/next page and open it +- gu: go to the parent directory +- gU: go to the root directory +- gi: focus the first input field + +Vim Vixen can be configured to follow links opened in tabs in the background +instead of switching to a new tab immediately. To do this, you'll need to update +the config file: change the `"background"` property of the `"follow.start"` +action to `true`, e.g.: + ```json { "keymaps": { - "F": { "type": "follow.start", "newTab": true, "background": true }, + "F": { "type": "follow.start", "newTab": true, "background": true } } } ``` #### Misc -- y: copy URL in current tab -- p: open clipboard's URL in current tab -- P: open clipboard's URL in new tab -- Shift+Esc: enable or disable the add-on in current tab. -- /: start to find a keyword in the page -- n: find next keyword in the page -- N: find prev keyword in the page -- gf: view page source +- y: copy the URL of the current tab to the clipboard +- p: open the clipboard's URL in the current tab +- P: open the clipboard's URL in new tab +- Shift+Esc: enable or disable the add-on in the current tab +- /: start searching for text in the page +- n: find the next search result in the page +- N: find the previous search result in the page +- gf: view the source of the current tab ### Console commands -Vim Vixen provides a console for `ex`-style commands similar to Vimperator. +Vim Vixen provides a console for `ex`-style commands, similar to Vimperator. -Open the console with :. Or start it with initial values using -o/O, t/T, -or w/W. +Open the console with :. Or populate it with initial values using +o/O, t/T, or +w/W. -#### `:open` command +#### `:open` The `:open` command operates two different ways, depending on the parameter. -When the parameter is a URL, that URL is opened in the current tab. +When the parameter is a URL, it's opened in the current tab. ``` :open http://github.com/ueokande ``` -Otherwise, the current tab will open a search page with the supplied string (defaults to Google). +Otherwise, the current tab opens a search page with the supplied string (defaults to Google). ``` :open How to contribute to Vim-Vixen ``` -To use a search engine other than the default, specify which search engine to use as the first parameter. +To use a search engine other than the default, specify the search engine to use as the first parameter. ``` :open yahoo How to contribute to Vim-Vixen ``` -To adjust the search engine default and add/remove search engines, see the [search engines](#search-engines) section. +To adjust the default search-engine and add/remove search engines, see the [search engines](#search-engines) section. -#### `:tabopen` command +#### `:tabopen` -Open a URL or search keywords by search engine in new tab. +Open a URL or search-engine query in a new tab. -#### `:quit` or `:q` command +#### `:quit` or `:q` Close the current tab. -#### `:quitall` or `:qa` command +#### `:quitall` or `:qa` Close all tabs. -#### `:bdelete` command +#### `:bdelete` Close a certain tab. -You can add `!` at the end of the command to close tab even if it is pinned: +You can add `!` to the end of the command to close a tab even if it is pinned: ``` :bdelete! ``` -#### `:bdeletes` command +#### `:bdeletes` -Close tabs matches with keywords. +Close tabs matching the specified keywords. -You can add `!` at the end of the command to close even pinned tabs: +You can add `!` to the end of the command to close pinned tabs: ``` :bdeletes! ``` -#### `:winopen` command +#### `:winopen` -Open a URL or search keywords by search engine in new window. +Open a URL or search-engine query in a new window. -#### `:buffer` command +#### `:buffer` -Select tabs by URL or title matched by keywords. +Select tabs by URL or title keywords. -#### `:addbookmark` command +#### `:addbookmark` Create a bookmark from the current URL. @@ -179,20 +184,19 @@ Create a bookmark from the current URL. :addbookmark My bookmark title ``` -The key map a is a convenient way to create a bookmark from the -current page. That shows `:addbookmark` with a title from the current page into -console. +The keymap a is a convenient way to create a bookmark for the +current page. It populates the console with `:addbookmark` and the title of +the current page. -#### `:set` command +#### `:set` -`:set` command can temporary override properties using console. See -[properties](#properties) section for more detailed description of available -properties. +The `:set` command can be used to temporarily override properties in the +console. See the [properties](#properties) section for more details on +the available properties. ### Properties -Plugin supports configurable properties which can be configured in JSON -settings: +Vim Vixen can be configured by defining settings in a JSON document, e.g.: ```json { @@ -202,33 +206,35 @@ settings: } ``` -Properties can be temporary overwritten by `:set` command in console. +Properties can be temporarily overridden by using the `:set` command in the +console. -List of available properties you can find below: +The following properties are available: -#### `smoothscroll` property +#### `smoothscroll` -Enable/disable smooth scroll. +Enable/disable smooth scrolling. ``` -:set smoothscroll " enable smooth scroll -:set nosmoothscroll " disable smooth scroll +:set smoothscroll " enable smooth scrolling +:set nosmoothscroll " disable smooth scrolling ``` -#### `hintchars` property +#### `hintchars` -Set hint characters +Set hint characters. ``` :set hintchars=0123456789 ``` -#### `complete` property +#### `complete` -Set completion items on `open`, `tabopen` `winopen` commands. +Set completion items on `open`, `tabopen`, and `winopen` commands. The allowed value is character sequence of `s`, `b`, or `h`. -Hit Tab or Shift+Tab to Select an item from the completion list. -Each character presents as following: +Hit Tab or Shift+Tab to select an item from the completion list. +Each character represents the following: + - `s`: search engines - `b`: bookmark items - `h`: history items. @@ -239,10 +245,10 @@ Each character presents as following: ### Search engines -Vim Vixen supports search by search engines like Google and Yahoo. +Vim Vixen supports searching with search engines such as Google and Yahoo. -You can configure search engines, including the default search engine, in the add-ons preferences. -The URLs specified in `"engines"` must contain a {}-placeholder, which will be +You can configure search engines, including the default search engine, in the add-on's preferences. +The URLs specified in `"engines"` must contain a `{}`-placeholder, which will be replaced with the search keyword parameters of the command. ```json @@ -264,7 +270,7 @@ replaced with the search keyword parameters of the command. ### Blacklist The blacklist allows you to disable the plugin for certain pages by URL patterns. -For instance, when you describe `"*.slack.com"`, the plugin is disabled on any Slack rooms. +For instance, you could use `"*.slack.com"` to disable the plugin on all Slack channels. In addition, you can also specify path patterns, such as `"example.com/mail/*"`. ```json @@ -279,6 +285,14 @@ In addition, you can also specify path patterns, such as `"example.com/mail/*"`. You can toggle Vim Vixen between disabled and enabled with shift+Esc. +## Compatibility + +- Firefox 60 ESR + +## Copyright + +Copyright © 2017-2019 by Shin'ya Ueoka + ## Licence MIT diff --git a/e2e/settings.js b/e2e/settings.js index e09747f..e78add0 100644 --- a/e2e/settings.js +++ b/e2e/settings.js @@ -24,7 +24,8 @@ module.exports = { "G": { "type": "scroll.bottom" }, "$": { "type": "scroll.end" }, "d": { "type": "tabs.close" }, - "D": { "type": "tabs.close.right" }, + "D": { "type": "tabs.close", "select": "left" }, + "x$": { "type": "tabs.close.right" }, "!d": { "type": "tabs.close.force" }, "u": { "type": "tabs.reopen" }, "K": { "type": "tabs.prev", "count": 1 }, diff --git a/e2e/tab.test.js b/e2e/tab.test.js index 16d61ae..c82890e 100644 --- a/e2e/tab.test.js +++ b/e2e/tab.test.js @@ -55,18 +55,32 @@ describe("tab test", () => { await browser.windows.remove(win.id); }); - it('deletes tab by d', async () => { + it('deletes tab and selects right by d', async () => { + await browser.tabs.update(tabs[3].id, { active: true }); let body = await session.findElementByCSS('body'); await body.sendKeys('d'); let current = await browser.tabs.query({ windowId: win.id }); assert(current.length === tabs.length - 1); + assert(current[3].active); + assert(current[3].url === tabs[4].url); + }); + + it('deletes tab and selects left by D', async () => { + await browser.tabs.update(tabs[3].id, { active: true }); + let body = await session.findElementByCSS('body'); + await body.sendKeys(Key.Shift, 'D'); + + let current = await browser.tabs.query({ windowId: win.id }); + assert(current.length === tabs.length - 1); + assert(current[2].active); + assert(current[2].url === tabs[2].url); }); - it('deletes tabs to the right by D', async () => { + it('deletes all tabs to the right by x$', async () => { await browser.tabs.update(tabs[1].id, { active: true }); let body = await session.findElementByCSS('body'); - await body.sendKeys(Key.Shift, 'd'); + await body.sendKeys('x', '$'); let current = await browser.tabs.query({ windowId: win.id }); assert(current.length === 2); diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 51cff28..7a10ad6 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -32,7 +32,7 @@ export default class OperationController { doOperation(operation: operations.Operation): Promise { switch (operation.type) { case operations.TAB_CLOSE: - return this.tabUseCase.close(false); + return this.tabUseCase.close(false, operation.select === 'left'); case operations.TAB_CLOSE_RIGHT: return this.tabUseCase.closeRight(); case operations.TAB_CLOSE_FORCE: diff --git a/src/background/usecases/TabUseCase.ts b/src/background/usecases/TabUseCase.ts index 31112a9..386307e 100644 --- a/src/background/usecases/TabUseCase.ts +++ b/src/background/usecases/TabUseCase.ts @@ -12,11 +12,15 @@ export default class TabUseCase { ) { } - async close(force: boolean): Promise { + async close(force: boolean, selectLeft = false): Promise { let tab = await this.tabPresenter.getCurrent(); if (!force && tab.pinned) { return Promise.resolve(); } + if (selectLeft && tab.index > 0) { + let tabs = await this.tabPresenter.getAll(); + await this.tabPresenter.select(tabs[tab.index - 1].id as number); + } return this.tabPresenter.remove([tab.id as number]); } diff --git a/src/content/presenters/ScrollPresenter.ts b/src/content/presenters/ScrollPresenter.ts index c06efca..e83f172 100644 --- a/src/content/presenters/ScrollPresenter.ts +++ b/src/content/presenters/ScrollPresenter.ts @@ -90,7 +90,7 @@ class Scroller { clearTimeout(lastTimeoutId); lastTimeoutId = null; } - lastTimeoutId = setTimeout(resetScrolling, 100); + lastTimeoutId = window.setTimeout(resetScrolling, 100); } } diff --git a/src/settings/keymaps.ts b/src/settings/keymaps.ts index 33ad26d..24ba1a5 100644 --- a/src/settings/keymaps.ts +++ b/src/settings/keymaps.ts @@ -18,11 +18,11 @@ const fields = [ ['mark.set.prefix', 'Set mark at current position'], ['mark.jump.prefix', 'Jump to the mark'], ], [ - ['tabs.close', 'Close a tab'], - ['tabs.close.right', 'Close tabs to the right'], + ['tabs.close?{"select":"right"}', 'Close a tab'], + ['tabs.close.right', 'Close all tabs to the right'], ['tabs.reopen', 'Reopen closed tab'], - ['tabs.next', 'Select next Tab'], - ['tabs.prev', 'Select prev Tab'], + ['tabs.next', 'Select next tab'], + ['tabs.prev', 'Select prev tab'], ['tabs.first', 'Select first tab'], ['tabs.last', 'Select last tab'], ['tabs.reload?{"cache":false}', 'Reload current tab'], @@ -50,8 +50,8 @@ const fields = [ ['command.show', 'Open console'], ['command.show.open?{"alter":false}', 'Open URL'], ['command.show.open?{"alter":true}', 'Alter URL'], - ['command.show.tabopen?{"alter":false}', 'Open URL in new Tab'], - ['command.show.tabopen?{"alter":true}', 'Alter URL in new Tab'], + ['command.show.tabopen?{"alter":false}', 'Open URL in new tab'], + ['command.show.tabopen?{"alter":true}', 'Alter URL in new tab'], ['command.show.winopen?{"alter":false}', 'Open URL in new window'], ['command.show.winopen?{"alter":true}', 'Alter URL in new window'], ['command.show.buffer', 'Open buffer command'], diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts index 1c085cf..14a7d35 100644 --- a/src/shared/SettingData.ts +++ b/src/shared/SettingData.ts @@ -353,7 +353,8 @@ export const DefaultSettingData: SettingData = SettingData.valueOf({ "G": { "type": "scroll.bottom" }, "$": { "type": "scroll.end" }, "d": { "type": "tabs.close" }, - "D": { "type": "tabs.close.right" }, + "D": { "type": "tabs.close", "select": "left" }, + "x$": { "type": "tabs.close.right" }, "!d": { "type": "tabs.close.force" }, "u": { "type": "tabs.reopen" }, "K": { "type": "tabs.prev" }, diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts index 0bef342..2a392df 100644 --- a/src/shared/Settings.ts +++ b/src/shared/Settings.ts @@ -146,7 +146,8 @@ export const DefaultSetting: Settings = { 'G': { 'type': 'scroll.bottom' }, '$': { 'type': 'scroll.end' }, 'd': { 'type': 'tabs.close' }, - 'D': { 'type': 'tabs.close.right' }, + 'D': { 'type': 'tabs.close', 'select': 'left' }, + 'x$': { 'type': 'tabs.close.right' }, '!d': { 'type': 'tabs.close.force' }, 'u': { 'type': 'tabs.reopen' }, 'K': { 'type': 'tabs.prev' }, diff --git a/src/shared/operations.ts b/src/shared/operations.ts index 2b03d9d..2df2e67 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -201,6 +201,7 @@ export interface PageHomeOperation { export interface TabCloseOperation { type: typeof TAB_CLOSE; + select?: 'left' | 'right'; } export interface TabCloseForceOperation { @@ -367,28 +368,41 @@ export type Operation = const assertOptionalBoolean = (obj: any, name: string) => { if (Object.prototype.hasOwnProperty.call(obj, name) && typeof obj[name] !== 'boolean') { - throw new TypeError(`Not a boolean parameter '${name}'`); + throw new TypeError(`Not a boolean parameter: '${name}'`); + } +}; + +const assertOptionalString = (obj: any, name: string, values?: string[]) => { + if (Object.prototype.hasOwnProperty.call(obj, name)) { + let value = obj[name]; + if (typeof value !== 'string') { + throw new TypeError(`Not a string parameter: '${name}'`); + } + if (values && values.length && values.indexOf(value) === -1) { + // eslint-disable-next-line max-len + throw new TypeError(`Invalid parameter for '${name}': '${value}'`); + } } }; const assertRequiredNumber = (obj: any, name: string) => { if (!Object.prototype.hasOwnProperty.call(obj, name) || typeof obj[name] !== 'number') { - throw new TypeError(`Missing number parameter '${name}`); + throw new TypeError(`Missing number parameter: '${name}`); } }; const assertRequiredString = (obj: any, name: string) => { if (!Object.prototype.hasOwnProperty.call(obj, name) || typeof obj[name] !== 'string') { - throw new TypeError(`Missing string parameter '${name}`); + throw new TypeError(`Missing string parameter: '${name}`); } }; // eslint-disable-next-line complexity, max-lines-per-function export const valueOf = (o: any): Operation => { if (!Object.prototype.hasOwnProperty.call(o, 'type')) { - throw new TypeError(`missing 'type' field`); + throw new TypeError(`Missing 'type' field`); } switch (o.type) { case COMMAND_SHOW_OPEN: @@ -416,6 +430,12 @@ export const valueOf = (o: any): Operation => { type: PAGE_HOME, newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), }; + case TAB_CLOSE: + assertOptionalString(o, 'select', ['left', 'right']); + return { + type: TAB_CLOSE, + select: (typeof o.select === undefined ? 'right' : o.select), + }; case TAB_RELOAD: assertOptionalBoolean(o, 'cache'); return { @@ -458,7 +478,6 @@ export const valueOf = (o: any): Operation => { case NAVIGATE_ROOT: case FOCUS_INPUT: case PAGE_SOURCE: - case TAB_CLOSE: case TAB_CLOSE_FORCE: case TAB_CLOSE_RIGHT: case TAB_REOPEN: @@ -483,5 +502,5 @@ export const valueOf = (o: any): Operation => { case REPEAT_LAST: return { type: o.type }; } - throw new TypeError('unknown operation type: ' + o.type); + throw new TypeError('Unknown operation type: ' + o.type); };