diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 1d1e4eb9f..b031cba33 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3745,6 +3745,21 @@ describe('TextEditorComponent', function () { return event } + function buildKeydownEvent ({keyCode, target}) { + let event = new KeyboardEvent('keydown') + Object.defineProperty(event, 'keyCode', { + get: function () { + return keyCode + } + }) + Object.defineProperty(event, 'target', { + get: function () { + return target + } + }) + return event + } + let inputNode beforeEach(function () { @@ -3767,11 +3782,12 @@ describe('TextEditorComponent', function () { expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {') }) - it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () { - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'u', - target: inputNode - })) + it('replaces the last character if a keypress event is bracketed by keydown events with matching keyCodes, which occurs when the accented character menu is shown', async function () { + componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) + componentNode.dispatchEvent(buildTextInputEvent({data: 'u', target: inputNode})) + componentNode.dispatchEvent(new KeyboardEvent('keypress')) + componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) + componentNode.dispatchEvent(new KeyboardEvent('keyup')) await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 9b091100d..12f481be5 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -247,9 +247,50 @@ class TextEditorComponent @scrollViewNode.addEventListener 'mousedown', @onMouseDown @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll + @detectAccentedCharacterMenu() @listenForIMEEvents() @trackSelectionClipboard() if process.platform is 'linux' + detectAccentedCharacterMenu: -> + # We need to get clever to detect when the accented character menu is + # opened on OS X. Usually, every keydown event that could cause input is + # followed by a corresponding keypress. However, pressing and holding + # long enough to open the accented character menu causes additional keydown + # events to fire that aren't followed by their own keypress and textInput + # events. + # + # Therefore, we assume the accented character menu has been deployed if, + # before observing any keyup event, we observe events in the following + # sequence: + # + # keydown(keyCode: X), keypress, keydown(keyCode: X) + # + # The keyCode X must be the same in the keydown events that bracket the + # keypress, meaning we're *holding* the _same_ key we intially pressed. + # Got that? + lastKeydown = null + lastKeydownBeforeKeypress = null + + @domNode.addEventListener 'keydown', (event) => + if lastKeydownBeforeKeypress + if lastKeydownBeforeKeypress.keyCode is event.keyCode + @openedAccentedCharacterMenu = true + lastKeydownBeforeKeypress = null + else + lastKeydown = event + + @domNode.addEventListener 'keypress', => + lastKeydownBeforeKeypress = lastKeydown + lastKeydown = null + + # This cancels the accented character behavior if we type a key normally + # with the menu open. + @openedAccentedCharacterMenu = false + + @domNode.addEventListener 'keyup', => + lastKeydownBeforeKeypress = null + lastKeydown = null + listenForIMEEvents: -> # The IME composition events work like this: # @@ -266,6 +307,9 @@ class TextEditorComponent checkpoint = null @domNode.addEventListener 'compositionstart', => + if @openedAccentedCharacterMenu + @editor.selectLeft() + @openedAccentedCharacterMenu = false checkpoint = @editor.createCheckpoint() @domNode.addEventListener 'compositionupdate', (event) => @editor.insertText(event.data, select: true) @@ -321,24 +365,21 @@ class TextEditorComponent onTextInput: (event) => event.stopPropagation() - - # If we prevent the insertion of a space character, then the browser - # interprets the spacebar keypress as a page-down command. - event.preventDefault() unless event.data is ' ' + event.preventDefault() return unless @isInputEnabled() - inputNode = event.target + # Workaround of the accented character suggestion feature in OS X. + # This will only occur when the user is not composing in IME mode. + # When the user selects a modified character from the OSX menu, `textInput` + # will occur twice, once for the initial character, and once for the + # modified character. However, only a single keypress will have fired. If + # this is the case, select backward to replace the original character. + if @openedAccentedCharacterMenu + @editor.selectLeft() + @openedAccentedCharacterMenu = false - # Work around of the accented character suggestion feature in OS X. - # Text input fires before a character is inserted, and if the browser is - # replacing the previous un-accented character with an accented variant, it - # will select backward over it. - selectedLength = inputNode.selectionEnd - inputNode.selectionStart - @editor.selectLeft() if selectedLength is 1 - - insertedRange = @editor.insertText(event.data, groupUndo: true) - inputNode.value = event.data if insertedRange + @editor.insertText(event.data, groupUndo: true) onVerticalScroll: (scrollTop) => return if @updateRequested or scrollTop is @presenter.getScrollTop()