From 250fe8a1107f056d6c759d5490a850d038714a7b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 6 May 2026 21:55:38 -0700 Subject: [PATCH 1/3] =?UTF-8?q?[autocomplete-plus]=20Ensure=20editor=20is?= =?UTF-8?q?=20re-focused=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …after the user selects a suggestion via the mouse. --- packages/autocomplete-plus/lib/suggestion-list.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/autocomplete-plus/lib/suggestion-list.js b/packages/autocomplete-plus/lib/suggestion-list.js index 08e401a58d..f1ede75088 100644 --- a/packages/autocomplete-plus/lib/suggestion-list.js +++ b/packages/autocomplete-plus/lib/suggestion-list.js @@ -334,6 +334,9 @@ class SuggestionList { // shouldn't remove this class name anymore. if (this.lastActiveAt > timestamp) return editorElement.classList.remove('autocomplete-active') + // If the user clicked on the suggestion, focus moved onto the overlay + // before it was destroyed, so we'll move it back onto the editor. + editorElement.focus() }) } this.suggestionMarker = undefined From 4081be4b383045fb7a8f38bdc5e6c0c177ea5007 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 9 May 2026 11:10:24 -0700 Subject: [PATCH 2/3] [autocomplete-plus] Fix failing spec --- packages/autocomplete-plus/lib/suggestion-list.js | 10 +++++++--- .../spec/autocomplete-manager-integration-spec.js | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/autocomplete-plus/lib/suggestion-list.js b/packages/autocomplete-plus/lib/suggestion-list.js index f1ede75088..5f70385ddf 100644 --- a/packages/autocomplete-plus/lib/suggestion-list.js +++ b/packages/autocomplete-plus/lib/suggestion-list.js @@ -326,7 +326,8 @@ class SuggestionList { } else if (this.overlayDecoration && this.overlayDecoration.destroy) { this.overlayDecoration.destroy() } - const editorElement = atom.views.getView(this.activeEditor) + const activeEditor = this.activeEditor + const editorElement = atom.views.getView(activeEditor) if (editorElement && editorElement.classList) { let timestamp = this.lastActiveAt atom.views.updateDocument(() => { @@ -335,8 +336,11 @@ class SuggestionList { if (this.lastActiveAt > timestamp) return editorElement.classList.remove('autocomplete-active') // If the user clicked on the suggestion, focus moved onto the overlay - // before it was destroyed, so we'll move it back onto the editor. - editorElement.focus() + // before it was destroyed, so we'll move it back onto the editor. But + // first we ensure that this is still the active editor! + if (atom.workspace.getActiveTextEditor() === activeEditor) { + editorElement.focus() + } }) } this.suggestionMarker = undefined diff --git a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js index b3736e1783..1c14228252 100644 --- a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js +++ b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js @@ -2350,7 +2350,7 @@ defm` expect(items[0].innerText.trim()).toEqual('center') }) - it('stops providing autocompletions when disposed.', async () => { + it('stops providing autocompletions when disposed', async () => { autocompleteDisposable.dispose() bottomEditorView.focus() triggerAutocompletion(bottomEditor) From c9f724e0bf5a33a3e803d301d7269212b0dc9a7b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 9 May 2026 11:30:46 -0700 Subject: [PATCH 3/3] [autocomplete-plus] Add spec to demonstrate the fix --- .../autocomplete-manager-integration-spec.js | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js index 1c14228252..2bed4a15c8 100644 --- a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js +++ b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js @@ -17,6 +17,12 @@ const path = require('path') let NodeTypeText = 3 +function simulateClick(element) { + element.dispatchEvent(new PointerEvent('mousedown', { bubbles: true, cancelable: true })); + element.dispatchEvent(new PointerEvent('mouseup', { bubbles: true, cancelable: true })); + element.dispatchEvent(new PointerEvent('click', { bubbles: true, cancelable: true })); +} + describe('Autocomplete Manager', () => { let autocompleteManager, editor, editorView, gutterWidth, mainModule, workspaceElement @@ -141,7 +147,7 @@ describe('Autocomplete Manager', () => { expect(editorView.querySelector('.autocomplete-plus')).not.toExist() }) - it('it refocuses the editor after pressing enter', async () => { + it('refocuses the editor after pressing enter', async () => { expect(editorView.querySelector('.autocomplete-plus')).not.toExist() editor.insertText('a') await waitForAutocomplete(editor) @@ -1311,6 +1317,30 @@ describe('Autocomplete Manager', () => { expect(editorView.querySelector('.autocomplete-plus')).not.toExist() }) + it('hides the suggestions list when a suggestion is clicked on', async () => { + triggerAutocompletion(editor, false, 'a') + await waitForAutocomplete(editor) + + expect(editorView.querySelector('.autocomplete-plus')).toExist() + + // Accept suggestion + let suggestionListView = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list') + let firstOption = suggestionListView.querySelector('li') + + // Manually blurring the editor here matches our observation that, when + // an actual human clicks on a suggestion, it blurs the editor and ends + // up focusing the BODY indirectly. + document.activeElement.blur() + simulateClick(firstOption) + + // Ensure the menu is closed… + expect(editorView.querySelector('.autocomplete-plus')).not.toExist() + // …and our editor still has focus. + await conditionPromise(() => { + return document.activeElement.closest('atom-text-editor') === editorView + }) + }) + describe('when the replacementPrefix is empty', () => { beforeEach(() => { spyOn(provider, 'getSuggestions').andCallFake(() => [{text: 'someMethod()', replacementPrefix: ''}])