Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 340fc63

Browse files
maxbrunsfeldNathan Sobo
authored andcommitted
Add character insertion timeouts, overlapping matches
* Handle overlapping partial matches for the same binding * Enable timeouts when first keystroke of a partial match corresponds to a character insertion Signed-off-by: Nathan Sobo <nathan@github.com>
1 parent 737a537 commit 340fc63

File tree

2 files changed

+51
-23
lines changed

2 files changed

+51
-23
lines changed

spec/keymap-manager-spec.coffee

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ describe "KeymapManager", ->
191191
'.editor.visual-mode': 'i w': 'select-inside-word'
192192

193193
events = []
194+
editor.addEventListener 'textInput', (event) -> events.push("input:#{event.data}")
194195
workspace.addEventListener 'dog', -> events.push('dog')
195196
workspace.addEventListener 'viva!', -> events.push('viva!')
196197
workspace.addEventListener 'viv', -> events.push('viv')
@@ -222,27 +223,15 @@ describe "KeymapManager", ->
222223
expect(vEvent.defaultPrevented).toBe true
223224
expect(iEvent.defaultPrevented).toBe true
224225
expect(kEvent.defaultPrevented).toBe false
225-
expect(events).toEqual ['enter-visual-mode']
226+
expect(events).toEqual ['enter-visual-mode', 'input:i']
226227
expect(clearTimeout).toHaveBeenCalled()
227228

228229
it "dispatches a text-input event for any replayed keyboard events that would have inserted characters", ->
229-
textInputCharacters = []
230-
editor.addEventListener 'textInput', (event) -> textInputCharacters.push(event.data)
231-
232-
keymapManager.handleKeyboardEvent(buildKeydownEvent('v', target: editor))
233-
keymapManager.handleKeyboardEvent(buildKeydownEvent('i', target: editor))
234-
keymapManager.handleKeyboardEvent(lastEvent = buildKeydownEvent('k', target: editor))
235-
236-
expect(textInputCharacters).toEqual ['i']
237-
expect(lastEvent.defaultPrevented).toBe false # inserted as normal
238-
239-
textInputCharacters = []
240-
241230
keymapManager.handleKeyboardEvent(buildKeydownEvent('d', target: editor))
242231
keymapManager.handleKeyboardEvent(buildKeydownEvent('o', target: editor))
243232
keymapManager.handleKeyboardEvent(lastEvent = buildKeydownEvent('q', target: editor))
244233

245-
expect(textInputCharacters).toEqual ['d', 'o']
234+
expect(events).toEqual ['input:d', 'input:o']
246235
expect(lastEvent.defaultPrevented).toBe false # inserted as normal
247236

248237
describe "when the currently queued keystrokes exactly match at least one binding", ->
@@ -257,7 +246,7 @@ describe "KeymapManager", ->
257246
keymapManager.handleKeyboardEvent(buildKeydownEvent('i', target: editor))
258247
expect(events).toEqual []
259248
advanceClock(keymapManager.getPartialMatchTimeout())
260-
expect(events).toEqual ['enter-visual-mode']
249+
expect(events).toEqual ['enter-visual-mode', 'input:i']
261250

262251
events = []
263252
keymapManager.handleKeyboardEvent(buildKeydownEvent('v', target: editor))
@@ -275,17 +264,31 @@ describe "KeymapManager", ->
275264
expect(global.setTimeout).not.toHaveBeenCalled()
276265
expect(keymapManager.queuedKeyboardEvents.length).toBe 0
277266

267+
describe "when the first queued keystroke corresponds to a character insertion", ->
268+
it "disables partially-matching bindings and replays the queued keystrokes if the ::partialMatchTimeout expires", ->
269+
keymapManager.handleKeyboardEvent(buildKeydownEvent('d', target: editor))
270+
keymapManager.handleKeyboardEvent(buildKeydownEvent('o', target: editor))
271+
advanceClock(keymapManager.getPartialMatchTimeout())
272+
273+
expect(events).toEqual ['input:d', 'input:o']
274+
278275
describe "when the currently queued keystrokes don't exactly match any bindings", ->
279276
it "never times out of the pending state", ->
280-
keymapManager.handleKeyboardEvent(buildKeydownEvent('d', target: editor))
277+
keymapManager.add 'test',
278+
'.workspace':
279+
'ctrl-d o g': 'control-dog'
280+
281+
workspace.addEventListener 'control-dog', -> events.push('control-dog')
282+
283+
keymapManager.handleKeyboardEvent(buildKeydownEvent('ctrl-d', target: editor))
281284
keymapManager.handleKeyboardEvent(buildKeydownEvent('o', target: editor))
282285

283286
advanceClock(keymapManager.getPartialMatchTimeout())
284287
advanceClock(keymapManager.getPartialMatchTimeout())
285288

286289
expect(events).toEqual []
287290
keymapManager.handleKeyboardEvent(buildKeydownEvent('g', target: editor))
288-
expect(events).toEqual ['dog']
291+
expect(events).toEqual ['control-dog']
289292

290293
describe "when the partially matching bindings all map to the 'unset!' directive", ->
291294
it "ignores the 'unset!' bindings and invokes the command associated with the matching binding as normal", ->
@@ -298,6 +301,16 @@ describe "KeymapManager", ->
298301

299302
expect(events).toEqual ['enter-visual-mode']
300303

304+
describe "when a subsequent keystroke begins a new match of an already pending binding", ->
305+
it "recognizes the match", ->
306+
keymapManager.handleKeyboardEvent(buildKeydownEvent('d', target: editor))
307+
keymapManager.handleKeyboardEvent(buildKeydownEvent('o', target: editor))
308+
keymapManager.handleKeyboardEvent(buildKeydownEvent('d', target: editor))
309+
keymapManager.handleKeyboardEvent(buildKeydownEvent('o', target: editor))
310+
keymapManager.handleKeyboardEvent(buildKeydownEvent('g', target: editor))
311+
312+
expect(events).toEqual ['input:d', 'input:o', 'dog']
313+
301314
it "only counts entire keystrokes when checking for partial matches", ->
302315
element = $$ -> @div class: 'a'
303316
keymapManager.add 'test',

src/keymap-manager.coffee

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ class KeymapManager
389389
# target is `.defaultTarget` if that property is assigned on the keymap.
390390
#
391391
# * `event` A `KeyboardEvent` of type 'keydown'
392-
handleKeyboardEvent: (event, replaying) ->
392+
handleKeyboardEvent: (event) ->
393393
keystroke = @keystrokeForKeyboardEvent(event)
394394

395395
if @queuedKeystrokes.length > 0 and isAtomModifier(keystroke)
@@ -453,7 +453,12 @@ class KeymapManager
453453
# keystroke.
454454
if partialMatches.length > 0
455455
event.preventDefault()
456-
enableTimeout = foundMatch ? @pendingStateTimeoutHandle?
456+
enableTimeout = (
457+
@pendingStateTimeoutHandle? or
458+
foundMatch or
459+
characterForKeyboardEvent(@queuedKeyboardEvents[0])?
460+
)
461+
457462
@enterPendingState(partialMatches, enableTimeout)
458463
@emitter.emit 'did-partially-match-binding', {
459464
keystrokes,
@@ -556,7 +561,7 @@ class KeymapManager
556561
@cancelPendingState() if @pendingStateTimeoutHandle?
557562
@pendingPartialMatches = pendingPartialMatches
558563
if enableTimeout
559-
@pendingStateTimeoutHandle = setTimeout(@terminatePendingState.bind(this), @partialMatchTimeout)
564+
@pendingStateTimeoutHandle = setTimeout(@terminatePendingState.bind(this, true), @partialMatchTimeout)
560565

561566
cancelPendingState: ->
562567
clearTimeout(@pendingStateTimeoutHandle)
@@ -568,16 +573,26 @@ class KeymapManager
568573
# It disables the longest of the pending partially matching bindings, then
569574
# replays the queued keyboard events to allow any bindings with shorter
570575
# keystroke sequences to be matched unambiguously.
571-
terminatePendingState: ->
576+
terminatePendingState: (timeout) ->
572577
bindingsToDisable = @pendingPartialMatches
573578
eventsToReplay = @queuedKeyboardEvents
574579

575580
@cancelPendingState()
576581
@clearQueuedKeystrokes()
577582

578583
binding.enabled = false for binding in bindingsToDisable
579-
@handleKeyboardEvent(event, true) for event in eventsToReplay
580-
binding.enabled = true for binding in bindingsToDisable
584+
585+
for event in eventsToReplay
586+
@handleKeyboardEvent(event)
587+
if bindingsToDisable? and not @pendingPartialMatches?
588+
binding.enabled = true for binding in bindingsToDisable
589+
bindingsToDisable = null
590+
591+
atom?.assert(not bindingsToDisable?, "Invalid keymap state")
592+
593+
if timeout and @pendingPartialMatches?
594+
@terminatePendingState(true)
595+
581596
return
582597

583598
# After we match a binding, we call this method to dispatch a custom event

0 commit comments

Comments
 (0)