diff options
Diffstat (limited to 'src/content')
| -rw-r--r-- | src/content/client/OperationClient.ts | 6 | ||||
| -rw-r--r-- | src/content/controllers/KeymapController.ts | 125 | ||||
| -rw-r--r-- | src/content/domains/KeySequence.ts | 91 | ||||
| -rw-r--r-- | src/content/repositories/KeymapRepository.ts | 2 | ||||
| -rw-r--r-- | src/content/usecases/KeymapUseCase.ts | 68 | 
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 { | 
