From 6605d3ea99c364a8f5f9d98a33e3ac89afb035f7 Mon Sep 17 00:00:00 2001 From: chocolateboy Date: Sun, 28 Jul 2019 05:35:07 +0100 Subject: [PATCH] Add an option to close the current tab and select the tab to the left Add an option to tabs.close to close the current tab and select the tab to the left. Bound to `D` by default, which replaces the tabs.close.right command, which is rarely-used. [1] The old `D` behavior has been moved to `gd`. + update the README and fix some lint errors [1] https://tinyurl.com/y4mj7hjy --- README.md | 216 ++++++++++-------- e2e/settings.js | 3 +- e2e/tab.test.js | 20 +- .../controllers/OperationController.ts | 2 +- src/background/usecases/TabUseCase.ts | 6 +- src/content/presenters/ScrollPresenter.ts | 2 +- src/settings/keymaps.ts | 12 +- src/shared/SettingData.ts | 3 +- src/shared/Settings.ts | 3 +- src/shared/operations.ts | 18 +- 10 files changed, 164 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 584656f..989fe9f 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,16 @@ 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 52+ +- Firefox for Android +- Waterfox 56 + +## Copyright + +Copyright © 2017-2019 by Shin'ya Ueoka + ## Licence MIT diff --git a/e2e/settings.js b/e2e/settings.js index e09747f..e3e9787 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", "selectLeft": true }, + "gd": { "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..aaaa5d7 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 gD', 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('g', Key.Shift, 'D'); 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..9480ed5 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.selectLeft); 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..b9a08e6 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?{"selectLeft":false}', '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..94d1e21 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", "selectLeft": true }, + "gd": { "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..c51cbc5 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', 'selectLeft': true }, + 'gd': { '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..f657db1 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; + selectLeft?: boolean; } export interface TabCloseForceOperation { @@ -367,28 +368,28 @@ 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 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 +417,12 @@ export const valueOf = (o: any): Operation => { type: PAGE_HOME, newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), }; + case TAB_CLOSE: + assertOptionalBoolean(o, 'selectLeft'); + return { + type: TAB_CLOSE, + selectLeft: Boolean(typeof o.selectLeft === undefined ? false : o.selectLeft), // eslint-disable-line max-len + }; case TAB_RELOAD: assertOptionalBoolean(o, 'cache'); return { @@ -458,7 +465,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 +489,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); };