aboutsummaryrefslogtreecommitdiff
path: root/src/content
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/content
parent3c7230c3036e8bb2b2e9a752be9b0ef4a0a7349d (diff)
parent75f86907fc2699c0f0661d4780c38249a18f849b (diff)
Merge pull request #689 from ueokande/n-times-repeat-operations
Repeat commands n-times
Diffstat (limited to 'src/content')
-rw-r--r--src/content/client/OperationClient.ts6
-rw-r--r--src/content/controllers/KeymapController.ts125
-rw-r--r--src/content/domains/KeySequence.ts91
-rw-r--r--src/content/repositories/KeymapRepository.ts2
-rw-r--r--src/content/usecases/KeymapUseCase.ts68
5 files changed, 194 insertions, 98 deletions
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/content/domains/KeySequence.ts b/src/content/domains/KeySequence.ts
new file mode 100644
index 0000000..4534b60
--- /dev/null
+++ b/src/content/domains/KeySequence.ts
@@ -0,0 +1,91 @@
+import Key from '../../shared/settings/Key';
+
+export default class KeySequence {
+ constructor(
+ public readonly keys: Key[],
+ ) {
+ }
+
+ push(key: Key): number {
+ return this.keys.push(key);
+ }
+
+ length(): number {
+ return this.keys.length;
+ }
+
+ startsWith(o: KeySequence): boolean {
+ if (this.keys.length < o.keys.length) {
+ return false;
+ }
+ for (let i = 0; i < o.keys.length; ++i) {
+ if (!this.keys[i].equals(o.keys[i])) {
+ return false;
+ }
+ }
+ 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[],
+ ): Key[] => {
+ if (remaining.length === 0) {
+ return mappedKeys;
+ }
+
+ let nextPos = 1;
+ if (remaining.startsWith('<')) {
+ let ltPos = remaining.indexOf('>');
+ if (ltPos > 0) {
+ nextPos = ltPos + 1;
+ }
+ }
+
+ return fromMapKeysRecursive(
+ remaining.slice(nextPos),
+ mappedKeys.concat([Key.fromMapKey(remaining.slice(0, nextPos))])
+ );
+ };
+
+ let data = fromMapKeysRecursive(keys, []);
+ return new KeySequence(data);
+ }
+}
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 {