aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode_key_handler.coffee
blob: cca6b77a5ae6d832ff32a67c0aaa5fc0e5bf13a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# Example key mapping (@keyMapping):
#   i:
#     command: "enterInsertMode", ... # This is a registryEntry object (as too are the other commands).
#   g:
#     g:
#       command: "scrollToTop", ...
#     t:
#       command: "nextTab", ...
#
# This key-mapping structure is generated by Commands.generateKeyStateMapping() and may be arbitrarily deep.
# Observe that @keyMapping["g"] is itself also a valid key mapping.  At any point, the key state (@keyState)
# consists of a (non-empty) list of such mappings.

class KeyHandlerMode extends Mode
  setKeyMapping: (@keyMapping) -> @reset()
  setPassKeys: (@passKeys) -> @reset()
  # Only for tests.
  setCommandHandler: (@commandHandler) ->

  # Reset the key state, optionally retaining the count provided.
  reset: (@countPrefix = 0) ->
    @keyState = [@keyMapping]

  constructor: (options) ->
    @commandHandler = options.commandHandler ? (->)
    @setKeyMapping options.keyMapping ? {}

    super extend options,
      keydown: @onKeydown.bind this

    if options.exitOnEscape
      # If we're part way through a command's key sequence, then a first Escape should reset the key state,
      # and only a second Escape should actually exit this mode.
      @push
        _name: "key-handler-escape-listener"
        keydown: (event) =>
          if KeyboardUtils.isEscape(event) and not @isInResetState()
            @reset()
            @suppressEvent
          else
            @continueBubbling

  onKeydown: (event) ->
    keyChar = KeyboardUtils.getKeyCharString event
    isEscape = KeyboardUtils.isEscape event
    if isEscape and (@countPrefix != 0 or @keyState.length != 1)
      DomUtils.consumeKeyup event, => @reset()
    # If the help dialog loses the focus, then Escape should hide it; see point 2 in #2045.
    else if isEscape and HelpDialog?.isShowing()
      HelpDialog.toggle()
      @suppressEvent
    else if isEscape
      @continueBubbling
    else if @isMappedKey keyChar
      @handleKeyChar keyChar
      @suppressEvent
    else if @isCountKey keyChar
      digit = parseInt keyChar
      @reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
      @suppressEvent
    else
      @reset() if keyChar
      @continueBubbling

  # This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).
  isMappedKey: (keyChar) ->
    (mapping for mapping in @keyState when keyChar of mapping)[0]? and not @isPassKey keyChar

  # This tests whether keyChar is a digit (and accounts for pass keys).
  isCountKey: (keyChar) ->
    keyChar and (if 0 < @countPrefix then '0' else '1') <= keyChar <= '9' and not @isPassKey keyChar

  # Keystrokes are *never* considered pass keys if the user has begun entering a command.  So, for example, if
  # 't' is a passKey, then the "t"-s of 'gt' and '99t' are neverthless handled as regular keys.
  isPassKey: (keyChar) ->
    @isInResetState() and keyChar in (@passKeys ? "")

  isInResetState: ->
    @countPrefix == 0 and @keyState.length == 1

  handleKeyChar: (keyChar) ->
    bgLog "handle key #{keyChar} (#{@name})"
    # A count prefix applies only so long a keyChar is mapped in @keyState[0]; e.g. 7gj should be 1j.
    @countPrefix = 0 unless keyChar of @keyState[0]
    # Advance the key state.  The new key state is the current mappings of keyChar, plus @keyMapping.
    @keyState = [(mapping[keyChar] for mapping in @keyState when keyChar of mapping)..., @keyMapping]
    if @keyState[0].command?
      command = @keyState[0]
      count = if 0 < @countPrefix then @countPrefix else 1
      bgLog "  invoke #{command.command} count=#{count} "
      @reset()
      @commandHandler {command, count}
      @exit() if @options.count? and --@options.count <= 0
    @suppressEvent

root = exports ? (window.root ?= {})
root.KeyHandlerMode = KeyHandlerMode
extend window, root unless exports?