aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-12-22 10:47:00 +0900
committerGitHub <noreply@github.com>2019-12-22 10:47:00 +0900
commitb2dcdedad729ff7087867da50e20578f9fc8fb29 (patch)
tree033ecffbd7db9b6db8000464a68d748fcae1dc3d /src
parent3c7230c3036e8bb2b2e9a752be9b0ef4a0a7349d (diff)
parent75f86907fc2699c0f0661d4780c38249a18f849b (diff)
Merge pull request #689 from ueokande/n-times-repeat-operations
Repeat commands n-times
Diffstat (limited to 'src')
-rw-r--r--src/background/controllers/OperationController.ts175
-rw-r--r--src/background/infrastructures/ContentMessageListener.ts9
-rw-r--r--src/content/client/OperationClient.ts6
-rw-r--r--src/content/controllers/KeymapController.ts125
-rw-r--r--src/content/domains/KeySequence.ts (renamed from src/shared/settings/KeySequence.ts)39
-rw-r--r--src/content/repositories/KeymapRepository.ts2
-rw-r--r--src/content/usecases/KeymapUseCase.ts68
-rw-r--r--src/shared/messages.ts1
-rw-r--r--src/shared/operations.ts26
-rw-r--r--src/shared/settings/Key.ts22
10 files changed, 284 insertions, 189 deletions
diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts
index 7a10ad6..2f5d4a6 100644
--- a/src/background/controllers/OperationController.ts
+++ b/src/background/controllers/OperationController.ts
@@ -21,95 +21,108 @@ export default class OperationController {
) {
}
- async exec(op: operations.Operation): Promise<any> {
- await this.doOperation(op);
+ async exec(repeat: number, op: operations.Operation): Promise<any> {
+ await this.doOperation(repeat, op);
if (this.repeatUseCase.isRepeatable(op)) {
this.repeatUseCase.storeLastOperation(op);
}
}
// eslint-disable-next-line complexity, max-lines-per-function
- doOperation(operation: operations.Operation): Promise<any> {
- switch (operation.type) {
- case operations.TAB_CLOSE:
- return this.tabUseCase.close(false, operation.select === 'left');
- case operations.TAB_CLOSE_RIGHT:
- return this.tabUseCase.closeRight();
- case operations.TAB_CLOSE_FORCE:
- return this.tabUseCase.close(true);
- case operations.TAB_REOPEN:
- return this.tabUseCase.reopen();
- case operations.TAB_PREV:
- return this.tabSelectUseCase.selectPrev(1);
- case operations.TAB_NEXT:
- return this.tabSelectUseCase.selectNext(1);
- case operations.TAB_FIRST:
- return this.tabSelectUseCase.selectFirst();
- case operations.TAB_LAST:
- return this.tabSelectUseCase.selectLast();
- case operations.TAB_PREV_SEL:
- return this.tabSelectUseCase.selectPrevSelected();
- case operations.TAB_RELOAD:
- return this.tabUseCase.reload(operation.cache);
- case operations.TAB_PIN:
- return this.tabUseCase.setPinned(true);
- case operations.TAB_UNPIN:
- return this.tabUseCase.setPinned(false);
- case operations.TAB_TOGGLE_PINNED:
- return this.tabUseCase.togglePinned();
- case operations.TAB_DUPLICATE:
- return this.tabUseCase.duplicate();
- case operations.PAGE_SOURCE:
- return this.tabUseCase.openPageSource();
- case operations.PAGE_HOME:
- return this.tabUseCase.openHome(operation.newTab);
- case operations.ZOOM_IN:
- return this.zoomUseCase.zoomIn();
- case operations.ZOOM_OUT:
- return this.zoomUseCase.zoomOut();
- case operations.ZOOM_NEUTRAL:
- return this.zoomUseCase.zoomNutoral();
- case operations.COMMAND_SHOW:
- return this.consoleUseCase.showCommand();
- case operations.COMMAND_SHOW_OPEN:
- return this.consoleUseCase.showOpenCommand(operation.alter);
- case operations.COMMAND_SHOW_TABOPEN:
- return this.consoleUseCase.showTabopenCommand(operation.alter);
- case operations.COMMAND_SHOW_WINOPEN:
- return this.consoleUseCase.showWinopenCommand(operation.alter);
- case operations.COMMAND_SHOW_BUFFER:
- return this.consoleUseCase.showBufferCommand();
- case operations.COMMAND_SHOW_ADDBOOKMARK:
- return this.consoleUseCase.showAddbookmarkCommand(operation.alter);
- case operations.FIND_START:
- return this.findUseCase.findStart();
- case operations.CANCEL:
- return this.consoleUseCase.hideConsole();
- case operations.NAVIGATE_HISTORY_PREV:
- return this.navigateUseCase.openHistoryPrev();
- case operations.NAVIGATE_HISTORY_NEXT:
- return this.navigateUseCase.openHistoryNext();
- case operations.NAVIGATE_LINK_PREV:
- return this.navigateUseCase.openLinkPrev();
- case operations.NAVIGATE_LINK_NEXT:
- return this.navigateUseCase.openLinkNext();
- case operations.NAVIGATE_PARENT:
- return this.navigateUseCase.openParent();
- case operations.NAVIGATE_ROOT:
- return this.navigateUseCase.openRoot();
- case operations.REPEAT_LAST:
- {
- let last = this.repeatUseCase.getLastOperation();
- if (typeof last !== 'undefined') {
- return this.doOperation(last);
+ async doOperation(
+ repeat: number,
+ operation: operations.Operation,
+ ): Promise<any> {
+ // eslint-disable-next-line complexity, max-lines-per-function
+ const opFunc = (() => {
+ switch (operation.type) {
+ case operations.TAB_CLOSE:
+ return () => this.tabUseCase.close(false, operation.select === 'left');
+ case operations.TAB_CLOSE_RIGHT:
+ return () => this.tabUseCase.closeRight();
+ case operations.TAB_CLOSE_FORCE:
+ return () => this.tabUseCase.close(true);
+ case operations.TAB_REOPEN:
+ return () => this.tabUseCase.reopen();
+ case operations.TAB_PREV:
+ return () => this.tabSelectUseCase.selectPrev(1);
+ case operations.TAB_NEXT:
+ return () => this.tabSelectUseCase.selectNext(1);
+ case operations.TAB_FIRST:
+ return () => this.tabSelectUseCase.selectFirst();
+ case operations.TAB_LAST:
+ return () => this.tabSelectUseCase.selectLast();
+ case operations.TAB_PREV_SEL:
+ return () => this.tabSelectUseCase.selectPrevSelected();
+ case operations.TAB_RELOAD:
+ return () => this.tabUseCase.reload(operation.cache);
+ case operations.TAB_PIN:
+ return () => this.tabUseCase.setPinned(true);
+ case operations.TAB_UNPIN:
+ return () => this.tabUseCase.setPinned(false);
+ case operations.TAB_TOGGLE_PINNED:
+ return () => this.tabUseCase.togglePinned();
+ case operations.TAB_DUPLICATE:
+ return () => this.tabUseCase.duplicate();
+ case operations.PAGE_SOURCE:
+ return () => this.tabUseCase.openPageSource();
+ case operations.PAGE_HOME:
+ return () => this.tabUseCase.openHome(operation.newTab);
+ case operations.ZOOM_IN:
+ return () => this.zoomUseCase.zoomIn();
+ case operations.ZOOM_OUT:
+ return () => this.zoomUseCase.zoomOut();
+ case operations.ZOOM_NEUTRAL:
+ return () => this.zoomUseCase.zoomNutoral();
+ case operations.COMMAND_SHOW:
+ return () => this.consoleUseCase.showCommand();
+ case operations.COMMAND_SHOW_OPEN:
+ return () => this.consoleUseCase.showOpenCommand(operation.alter);
+ case operations.COMMAND_SHOW_TABOPEN:
+ return () => this.consoleUseCase.showTabopenCommand(operation.alter);
+ case operations.COMMAND_SHOW_WINOPEN:
+ return () => this.consoleUseCase.showWinopenCommand(operation.alter);
+ case operations.COMMAND_SHOW_BUFFER:
+ return () => this.consoleUseCase.showBufferCommand();
+ case operations.COMMAND_SHOW_ADDBOOKMARK:
+ return () => this.consoleUseCase.showAddbookmarkCommand(
+ operation.alter);
+ case operations.FIND_START:
+ return () => this.findUseCase.findStart();
+ case operations.CANCEL:
+ return () => this.consoleUseCase.hideConsole();
+ case operations.NAVIGATE_HISTORY_PREV:
+ return () => this.navigateUseCase.openHistoryPrev();
+ case operations.NAVIGATE_HISTORY_NEXT:
+ return () => this.navigateUseCase.openHistoryNext();
+ case operations.NAVIGATE_LINK_PREV:
+ return () => this.navigateUseCase.openLinkPrev();
+ case operations.NAVIGATE_LINK_NEXT:
+ return () => this.navigateUseCase.openLinkNext();
+ case operations.NAVIGATE_PARENT:
+ return () => this.navigateUseCase.openParent();
+ case operations.NAVIGATE_ROOT:
+ return () => this.navigateUseCase.openRoot();
+ case operations.REPEAT_LAST:
+ return () => {
+ let last = this.repeatUseCase.getLastOperation();
+ if (typeof last !== 'undefined') {
+ return this.doOperation(1, last);
+ }
+ return Promise.resolve();
+ };
+ case operations.INTERNAL_OPEN_URL:
+ return () => this.tabUseCase.openURL(
+ operation.url, operation.newTab, operation.newWindow);
+ default:
+ throw new Error('unknown operation: ' + operation.type);
}
- return Promise.resolve();
- }
- case operations.INTERNAL_OPEN_URL:
- return this.tabUseCase.openURL(
- operation.url, operation.newTab, operation.newWindow);
+ })();
+
+ for (let i = 0; i < repeat; ++i) {
+ // eslint-disable-next-line no-await-in-loop
+ await opFunc();
}
- throw new Error('unknown operation: ' + operation.type);
}
}
diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts
index f80d686..f20340b 100644
--- a/src/background/infrastructures/ContentMessageListener.ts
+++ b/src/background/infrastructures/ContentMessageListener.ts
@@ -1,5 +1,6 @@
import { injectable } from 'tsyringe';
import * as messages from '../../shared/messages';
+import * as operations from '../../shared/operations';
import CompletionGroup from '../domains/CompletionGroup';
import CommandController from '../controllers/CommandController';
import SettingController from '../controllers/SettingController';
@@ -19,7 +20,7 @@ export default class ContentMessageListener {
private findController: FindController,
private addonEnabledController: AddonEnabledController,
private linkController: LinkController,
- private backgroundOperationController: OperationController,
+ private operationController: OperationController,
private markController: MarkController,
) {
this.consolePorts = {};
@@ -79,7 +80,7 @@ export default class ContentMessageListener {
senderTab.id as number,
message.background);
case messages.BACKGROUND_OPERATION:
- return this.onBackgroundOperation(message.operation);
+ return this.onBackgroundOperation(message.repeat, message.operation);
case messages.MARK_SET_GLOBAL:
return this.onMarkSetGlobal(message.key, message.x, message.y);
case messages.MARK_JUMP_GLOBAL:
@@ -126,8 +127,8 @@ export default class ContentMessageListener {
return this.linkController.openToTab(url, openerId);
}
- onBackgroundOperation(operation: any): Promise<any> {
- return this.backgroundOperationController.exec(operation);
+ onBackgroundOperation(count: number, op: operations.Operation): Promise<any> {
+ return this.operationController.exec(count, op);
}
onMarkSetGlobal(key: string, x: number, y: number): Promise<any> {
diff --git a/src/content/client/OperationClient.ts b/src/content/client/OperationClient.ts
index 5dbe555..9c72c75 100644
--- a/src/content/client/OperationClient.ts
+++ b/src/content/client/OperationClient.ts
@@ -2,7 +2,7 @@ import * as operations from '../../shared/operations';
import * as messages from '../../shared/messages';
export default interface OperationClient {
- execBackgroundOp(op: operations.Operation): Promise<void>;
+ execBackgroundOp(repeat: number, op: operations.Operation): Promise<void>;
internalOpenUrl(
url: string, newTab?: boolean, background?: boolean,
@@ -10,9 +10,10 @@ export default interface OperationClient {
}
export class OperationClientImpl implements OperationClient {
- execBackgroundOp(op: operations.Operation): Promise<void> {
+ execBackgroundOp(repeat: number, op: operations.Operation): Promise<void> {
return browser.runtime.sendMessage({
type: messages.BACKGROUND_OPERATION,
+ repeat,
operation: op,
});
}
@@ -22,6 +23,7 @@ export class OperationClientImpl implements OperationClient {
): Promise<void> {
return browser.runtime.sendMessage({
type: messages.BACKGROUND_OPERATION,
+ repeat: 1,
operation: {
type: operations.INTERNAL_OPEN_URL,
url,
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
index 6157a71..452e3d4 100644
--- a/src/content/controllers/KeymapController.ts
+++ b/src/content/controllers/KeymapController.ts
@@ -23,7 +23,7 @@ export default class KeymapController {
private markKeyUseCase: MarkKeyyUseCase,
@inject('OperationClient')
- private backgroundClient: OperationClient,
+ private operationClient: OperationClient,
@inject('FollowMasterClient')
private followMasterClient: FollowMasterClient,
@@ -32,71 +32,70 @@ export default class KeymapController {
// eslint-disable-next-line complexity, max-lines-per-function
press(key: Key): boolean {
- let op = this.keymapUseCase.nextOp(key);
- if (op === null) {
+ let nextOp = this.keymapUseCase.nextOps(key);
+ if (nextOp === null) {
return false;
}
- // do not await due to return a boolean immediately
- switch (op.type) {
- case operations.ADDON_ENABLE:
- this.addonEnabledUseCase.enable();
- break;
- case operations.ADDON_DISABLE:
- this.addonEnabledUseCase.disable();
- break;
- case operations.ADDON_TOGGLE_ENABLED:
- this.addonEnabledUseCase.toggle();
- break;
- case operations.FIND_NEXT:
- this.findSlaveUseCase.findNext();
- break;
- case operations.FIND_PREV:
- this.findSlaveUseCase.findPrev();
- break;
- case operations.SCROLL_VERTICALLY:
- this.scrollUseCase.scrollVertically(op.count);
- break;
- case operations.SCROLL_HORIZONALLY:
- this.scrollUseCase.scrollHorizonally(op.count);
- break;
- case operations.SCROLL_PAGES:
- this.scrollUseCase.scrollPages(op.count);
- break;
- case operations.SCROLL_TOP:
- this.scrollUseCase.scrollToTop();
- break;
- case operations.SCROLL_BOTTOM:
- this.scrollUseCase.scrollToBottom();
- break;
- case operations.SCROLL_HOME:
- this.scrollUseCase.scrollToHome();
- break;
- case operations.SCROLL_END:
- this.scrollUseCase.scrollToEnd();
- break;
- case operations.FOLLOW_START:
- this.followMasterClient.startFollow(op.newTab, op.background);
- break;
- case operations.MARK_SET_PREFIX:
- this.markKeyUseCase.enableSetMode();
- break;
- case operations.MARK_JUMP_PREFIX:
- this.markKeyUseCase.enableJumpMode();
- break;
- case operations.FOCUS_INPUT:
- this.focusUseCase.focusFirstInput();
- break;
- case operations.URLS_YANK:
- this.clipbaordUseCase.yankCurrentURL();
- break;
- case operations.URLS_PASTE:
- this.clipbaordUseCase.openOrSearch(
- op.newTab ? op.newTab : false,
- );
- break;
- default:
- this.backgroundClient.execBackgroundOp(op);
+ if (!operations.isNRepeatable(nextOp.op.type)) {
+ nextOp.repeat = 1;
+ }
+
+ const doFunc = ((op: operations.Operation) => {
+ switch (op.type) {
+ case operations.ADDON_ENABLE:
+ return () => this.addonEnabledUseCase.enable();
+ case operations.ADDON_DISABLE:
+ return () => this.addonEnabledUseCase.disable();
+ case operations.ADDON_TOGGLE_ENABLED:
+ return () => this.addonEnabledUseCase.toggle();
+ case operations.FIND_NEXT:
+ return () => this.findSlaveUseCase.findNext();
+ case operations.FIND_PREV:
+ return () => this.findSlaveUseCase.findPrev();
+ case operations.SCROLL_VERTICALLY:
+ return () => this.scrollUseCase.scrollVertically(op.count);
+ case operations.SCROLL_HORIZONALLY:
+ return () => this.scrollUseCase.scrollHorizonally(op.count);
+ case operations.SCROLL_PAGES:
+ return () => this.scrollUseCase.scrollPages(op.count);
+ case operations.SCROLL_TOP:
+ return () => this.scrollUseCase.scrollToTop();
+ case operations.SCROLL_BOTTOM:
+ return () => this.scrollUseCase.scrollToBottom();
+ case operations.SCROLL_HOME:
+ return () => this.scrollUseCase.scrollToHome();
+ case operations.SCROLL_END:
+ return () => this.scrollUseCase.scrollToEnd();
+ case operations.FOLLOW_START:
+ return () => this.followMasterClient.startFollow(
+ op.newTab, op.background);
+ case operations.MARK_SET_PREFIX:
+ return () => this.markKeyUseCase.enableSetMode();
+ case operations.MARK_JUMP_PREFIX:
+ return () => this.markKeyUseCase.enableJumpMode();
+ case operations.FOCUS_INPUT:
+ return () => this.focusUseCase.focusFirstInput();
+ case operations.URLS_YANK:
+ return () => this.clipbaordUseCase.yankCurrentURL();
+ case operations.URLS_PASTE:
+ return () => this.clipbaordUseCase.openOrSearch(
+ op.newTab ? op.newTab : false,
+ );
+ default:
+ return null;
+ }
+ })(nextOp.op);
+
+ if (doFunc === null) {
+ // Do not await asynchronous methods to return a boolean immidiately. The
+ // caller requires the synchronous response from the callback to identify
+ // to continue of abandon the event propagations.
+ this.operationClient.execBackgroundOp(nextOp.repeat, nextOp.op);
+ } else {
+ for (let i = 0; i < nextOp.repeat; ++i) {
+ doFunc();
+ }
}
return true;
}
diff --git a/src/shared/settings/KeySequence.ts b/src/content/domains/KeySequence.ts
index abae61a..4534b60 100644
--- a/src/shared/settings/KeySequence.ts
+++ b/src/content/domains/KeySequence.ts
@@ -1,4 +1,4 @@
-import Key from './Key';
+import Key from '../../shared/settings/Key';
export default class KeySequence {
constructor(
@@ -26,6 +26,43 @@ export default class KeySequence {
return true;
}
+ isDigitOnly(): boolean {
+ return this.keys.every(key => key.isDigit());
+ }
+
+ repeatCount(): number {
+ let nonDigitAt = this.keys.findIndex(key => !key.isDigit());
+ if (this.keys.length === 0 || nonDigitAt === 0) {
+ return 1;
+ }
+ if (nonDigitAt === -1) {
+ nonDigitAt = this.keys.length;
+ }
+ let digits = this.keys.slice(0, nonDigitAt)
+ .map(key => key.key)
+ .join('');
+ return Number(digits);
+ }
+
+ trimNumericPrefix(): KeySequence {
+ let nonDigitAt = this.keys.findIndex(key => !key.isDigit());
+ if (nonDigitAt === -1) {
+ nonDigitAt = this.keys.length;
+ }
+ return new KeySequence(this.keys.slice(nonDigitAt));
+ }
+
+ splitNumericPrefix(): [KeySequence, KeySequence] {
+ let nonDigitIndex = this.keys.findIndex(key => !key.isDigit());
+ if (nonDigitIndex === -1) {
+ return [this, new KeySequence([])];
+ }
+ return [
+ new KeySequence(this.keys.slice(0, nonDigitIndex)),
+ new KeySequence(this.keys.slice(nonDigitIndex)),
+ ];
+ }
+
static fromMapKeys(keys: string): KeySequence {
const fromMapKeysRecursive = (
remaining: string, mappedKeys: Key[],
diff --git a/src/content/repositories/KeymapRepository.ts b/src/content/repositories/KeymapRepository.ts
index 3391229..2944723 100644
--- a/src/content/repositories/KeymapRepository.ts
+++ b/src/content/repositories/KeymapRepository.ts
@@ -1,5 +1,5 @@
import Key from '../../shared/settings/Key';
-import KeySequence from '../../shared/settings/KeySequence';
+import KeySequence from '../domains/KeySequence';
export default interface KeymapRepository {
enqueueKey(key: Key): KeySequence;
diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts
index 67d667d..a2e7cc3 100644
--- a/src/content/usecases/KeymapUseCase.ts
+++ b/src/content/usecases/KeymapUseCase.ts
@@ -5,16 +5,19 @@ import AddonEnabledRepository from '../repositories/AddonEnabledRepository';
import * as operations from '../../shared/operations';
import Keymaps from '../../shared/settings/Keymaps';
import Key from '../../shared/settings/Key';
-import KeySequence from '../../shared/settings/KeySequence';
+import KeySequence from '../domains/KeySequence';
import AddressRepository from '../repositories/AddressRepository';
-type KeymapEntityMap = Map<KeySequence, operations.Operation>;
-
const reservedKeymaps = Keymaps.fromJSON({
'<Esc>': { type: operations.CANCEL },
'<C-[>': { type: operations.CANCEL },
});
+const enableAddonOps = [
+ operations.ADDON_ENABLE,
+ operations.ADDON_TOGGLE_ENABLED,
+];
+
@injectable()
export default class KeymapUseCase {
constructor(
@@ -32,53 +35,54 @@ export default class KeymapUseCase {
) {
}
- nextOp(key: Key): operations.Operation | null {
+ // eslint-disable-next-line max-statements
+ nextOps(key: Key): { repeat: number, op: operations.Operation } | null {
let sequence = this.repository.enqueueKey(key);
- if (sequence.length() === 1 && this.blacklistKey(key)) {
+ let baseSequence = sequence.trimNumericPrefix();
+ if (baseSequence.length() === 1 && this.blacklistKey(key)) {
// ignore if the input starts with black list keys
this.repository.clear();
return null;
}
let keymaps = this.keymapEntityMap();
- let matched = Array.from(keymaps.keys()).filter(
- (mapping: KeySequence) => {
- return mapping.startsWith(sequence);
- });
- if (!this.addonEnabledRepository.get()) {
- // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
- // the addon disabled
- matched = matched.filter((keymap) => {
- let type = (keymaps.get(keymap) as operations.Operation).type;
- return type === operations.ADDON_ENABLE ||
- type === operations.ADDON_TOGGLE_ENABLED;
- });
- }
- if (matched.length === 0) {
- // No operations to match with inputs
+ let matched = keymaps.filter(([seq]) => seq.startsWith(sequence));
+ let baseMatched = keymaps.filter(([seq]) => seq.startsWith(baseSequence));
+
+ if (matched.length === 1 &&
+ sequence.length() === matched[0][0].length()) {
+ // keys are matched with an operation
this.repository.clear();
- return null;
- } else if (matched.length > 1 ||
- matched.length === 1 && sequence.length() < matched[0].length()) {
- // More than one operations are matched
+ return { repeat: 1, op: matched[0][1] };
+ } else if (
+ baseMatched.length === 1 &&
+ baseSequence.length() === baseMatched[0][0].length()) {
+ // keys are matched with an operation with a numeric prefix
+ this.repository.clear();
+ return { repeat: sequence.repeatCount(), op: baseMatched[0][1] };
+ } else if (matched.length >= 1 || baseMatched.length >= 1) {
+ // keys are matched with an operation's prefix
return null;
}
- // Exactly one operation is matched
- let operation = keymaps.get(matched[0]) as operations.Operation;
- this.repository.clear();
- return operation;
- }
- clear(): void {
+ // matched with no operations
this.repository.clear();
+ return null;
}
- private keymapEntityMap(): KeymapEntityMap {
+ private keymapEntityMap(): [KeySequence, operations.Operation][] {
let keymaps = this.settingRepository.get().keymaps.combine(reservedKeymaps);
let entries = keymaps.entries().map(
([keys, op]) => [KeySequence.fromMapKeys(keys), op]
) as [KeySequence, operations.Operation][];
- return new Map<KeySequence, operations.Operation>(entries);
+ if (!this.addonEnabledRepository.get()) {
+ // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
+ // the addon disabled
+ entries = entries.filter(
+ ([_seq, { type }]) => enableAddonOps.includes(type)
+ );
+ }
+ return entries;
}
private blacklistKey(key: Key): boolean {
diff --git a/src/shared/messages.ts b/src/shared/messages.ts
index 36a23d8..7f8bd5b 100644
--- a/src/shared/messages.ts
+++ b/src/shared/messages.ts
@@ -49,6 +49,7 @@ export const NAVIGATE_LINK_PREV = 'navigate.link.prev';
export interface BackgroundOperationMessage {
type: typeof BACKGROUND_OPERATION;
+ repeat: number;
operation: operations.Operation;
}
diff --git a/src/shared/operations.ts b/src/shared/operations.ts
index 1ce5256..67c5ca8 100644
--- a/src/shared/operations.ts
+++ b/src/shared/operations.ts
@@ -508,3 +508,29 @@ export const valueOf = (o: any): Operation => {
}
throw new TypeError('Unknown operation type: ' + o.type);
};
+
+export const isNRepeatable = (type: string): boolean => {
+ switch (type) {
+ case SCROLL_VERTICALLY:
+ case SCROLL_HORIZONALLY:
+ case SCROLL_PAGES:
+ case NAVIGATE_HISTORY_PREV:
+ case NAVIGATE_HISTORY_NEXT:
+ case NAVIGATE_PARENT:
+ case TAB_CLOSE:
+ case TAB_CLOSE_FORCE:
+ case TAB_CLOSE_RIGHT:
+ case TAB_REOPEN:
+ case TAB_PREV:
+ case TAB_NEXT:
+ case TAB_DUPLICATE:
+ case ZOOM_IN:
+ case ZOOM_OUT:
+ case URLS_PASTE:
+ case FIND_NEXT:
+ case FIND_PREV:
+ case REPEAT_LAST:
+ return true;
+ }
+ return false;
+};
diff --git a/src/shared/settings/Key.ts b/src/shared/settings/Key.ts
index b11eeb2..3a3eb3b 100644
--- a/src/shared/settings/Key.ts
+++ b/src/shared/settings/Key.ts
@@ -1,3 +1,5 @@
+const digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
+
export default class Key {
public readonly key: string;
@@ -9,12 +11,18 @@ export default class Key {
public readonly meta: boolean;
- constructor({ key, shift, ctrl, alt, meta }: {
+ constructor({
+ key,
+ shift = false,
+ ctrl = false,
+ alt = false,
+ meta = false,
+ }: {
key: string;
- shift: boolean;
- ctrl: boolean;
- alt: boolean;
- meta: boolean;
+ shift?: boolean;
+ ctrl?: boolean;
+ alt?: boolean;
+ meta?: boolean;
}) {
this.key = key;
this.shift = shift;
@@ -51,6 +59,10 @@ export default class Key {
});
}
+ isDigit(): boolean {
+ return digits.includes(this.key);
+ }
+
equals(key: Key) {
return this.key === key.key &&
this.ctrl === key.ctrl &&