Editors can steal focus later when opening slowly (fix #128117) (#170328)

* Editors can steal focus later when opening slowly (fix #128117)

* play it a bit safer

* fix tests
This commit is contained in:
Benjamin Pasero 2023-01-01 16:31:26 +01:00 committed by GitHub
parent 4c0cecf300
commit f01a44b447
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 58 deletions

View file

@ -71,9 +71,7 @@ suite('vscode API - window', () => {
reg.dispose(); reg.dispose();
}); });
test('editor, onDidChangeTextEditorViewColumn (close editor)', () => { test('editor, onDidChangeTextEditorViewColumn (close editor)', async () => {
let actualEvent: TextEditorViewColumnChangeEvent;
const registration1 = workspace.registerTextDocumentContentProvider('bikes', { const registration1 = workspace.registerTextDocumentContentProvider('bikes', {
provideTextDocumentContent() { provideTextDocumentContent() {
@ -81,33 +79,30 @@ suite('vscode API - window', () => {
} }
}); });
return Promise.all([ const doc1 = await workspace.openTextDocument(Uri.parse('bikes://testing/one'));
workspace.openTextDocument(Uri.parse('bikes://testing/one')).then(doc => window.showTextDocument(doc, ViewColumn.One)), await window.showTextDocument(doc1, ViewColumn.One);
workspace.openTextDocument(Uri.parse('bikes://testing/two')).then(doc => window.showTextDocument(doc, ViewColumn.Two))
]).then(async editors => {
const [one, two] = editors; const doc2 = await workspace.openTextDocument(Uri.parse('bikes://testing/two'));
const two = await window.showTextDocument(doc2, ViewColumn.Two);
await new Promise<void>(resolve => { assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.Two);
const registration2 = window.onDidChangeTextEditorViewColumn(event => {
actualEvent = event; const actualEvent = await new Promise<TextEditorViewColumnChangeEvent>(resolve => {
registration2.dispose(); const registration2 = window.onDidChangeTextEditorViewColumn(event => {
resolve(); registration2.dispose();
}); resolve(event);
// close editor 1, wait a little for the event to bubble
one.hide();
}); });
assert.ok(actualEvent); // close editor 1, wait a little for the event to bubble
assert.ok(actualEvent.textEditor === two); commands.executeCommand('workbench.action.closeEditorsInOtherGroups');
assert.ok(actualEvent.viewColumn === two.viewColumn);
registration1.dispose();
}); });
assert.ok(actualEvent);
assert.ok(actualEvent.textEditor === two);
assert.ok(actualEvent.viewColumn === two.viewColumn);
registration1.dispose();
}); });
test('editor, onDidChangeTextEditorViewColumn (move editor group)', () => { test('editor, onDidChangeTextEditorViewColumn (move editor group)', async () => {
const actualEvents: TextEditorViewColumnChangeEvent[] = [];
const registration1 = workspace.registerTextDocumentContentProvider('bikes', { const registration1 = workspace.registerTextDocumentContentProvider('bikes', {
provideTextDocumentContent() { provideTextDocumentContent() {
@ -115,38 +110,38 @@ suite('vscode API - window', () => {
} }
}); });
return Promise.all([ const doc1 = await workspace.openTextDocument(Uri.parse('bikes://testing/one'));
workspace.openTextDocument(Uri.parse('bikes://testing/one')).then(doc => window.showTextDocument(doc, ViewColumn.One)), await window.showTextDocument(doc1, ViewColumn.One);
workspace.openTextDocument(Uri.parse('bikes://testing/two')).then(doc => window.showTextDocument(doc, ViewColumn.Two))
]).then(editors => {
const [, two] = editors; const doc2 = await workspace.openTextDocument(Uri.parse('bikes://testing/two'));
two.show(); await window.showTextDocument(doc2, ViewColumn.Two);
return new Promise<void>(resolve => { assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.Two);
const registration2 = window.onDidChangeTextEditorViewColumn(event => { const actualEvents = await new Promise<TextEditorViewColumnChangeEvent[]>(resolve => {
actualEvents.push(event);
if (actualEvents.length === 2) { const actualEvents: TextEditorViewColumnChangeEvent[] = [];
registration2.dispose();
resolve();
}
});
// move active editor group left const registration2 = window.onDidChangeTextEditorViewColumn(event => {
return commands.executeCommand('workbench.action.moveActiveEditorGroupLeft'); actualEvents.push(event);
}).then(() => { if (actualEvents.length === 2) {
assert.strictEqual(actualEvents.length, 2); registration2.dispose();
resolve(actualEvents);
for (const event of actualEvents) {
assert.strictEqual(event.viewColumn, event.textEditor.viewColumn);
} }
registration1.dispose();
}); });
// move active editor group left
return commands.executeCommand('workbench.action.moveActiveEditorGroupLeft');
}); });
assert.strictEqual(actualEvents.length, 2);
for (const event of actualEvents) {
assert.strictEqual(event.viewColumn, event.textEditor.viewColumn);
}
registration1.dispose();
}); });
test('active editor not always correct... #49125', async function () { test('active editor not always correct... #49125', async function () {

View file

@ -210,7 +210,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.element.appendChild(this.editorContainer); this.element.appendChild(this.editorContainer);
// Editor pane // Editor pane
this.editorPane = this._register(this.scopedInstantiationService.createInstance(EditorPanes, this.editorContainer, this)); this.editorPane = this._register(this.scopedInstantiationService.createInstance(EditorPanes, this.element, this.editorContainer, this));
this._onDidChange.input = this.editorPane.onDidChangeSizeConstraints; this._onDidChange.input = this.editorPane.onDidChangeSizeConstraints;
// Track Focus // Track Focus
@ -511,6 +511,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// not changed meanwhile. This prevents focus from being // not changed meanwhile. This prevents focus from being
// stolen accidentally on startup when the user already // stolen accidentally on startup when the user already
// clicked somewhere. // clicked somewhere.
if (this.accessor.activeGroup === this && activeElement === document.activeElement) { if (this.accessor.activeGroup === this && activeElement === document.activeElement) {
this.focus(); this.focus();
} }

View file

@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, isEditorOpenError } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { Dimension, show, hide, IDomNodePagePosition } from 'vs/base/browser/dom'; import { Dimension, show, hide, IDomNodePagePosition, isAncestor } from 'vs/base/browser/dom';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@ -91,7 +91,8 @@ export class EditorPanes extends Disposable {
private readonly editorPanesRegistry = Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane); private readonly editorPanesRegistry = Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane);
constructor( constructor(
private parent: HTMLElement, private editorGroupParent: HTMLElement,
private editorPanesParent: HTMLElement,
private groupView: IEditorGroupView, private groupView: IEditorGroupView,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@ -237,20 +238,57 @@ export class EditorPanes extends Disposable {
// Editor pane // Editor pane
const pane = this.doShowEditorPane(descriptor); const pane = this.doShowEditorPane(descriptor);
// Remember current active element for deciding to restore focus later
const activeElement = document.activeElement;
// Apply input to pane // Apply input to pane
const { changed, cancelled } = await this.doSetInput(pane, editor, options, context); const { changed, cancelled } = await this.doSetInput(pane, editor, options, context);
// Focus unless cancelled // Focus only if not cancelled and not prevented
if (!cancelled) { const focus = !options || !options.preserveFocus;
const focus = !options || !options.preserveFocus; if (!cancelled && focus && this.shouldRestoreFocus(activeElement)) {
if (focus) { pane.focus();
pane.focus();
}
} }
return { pane, changed, cancelled }; return { pane, changed, cancelled };
} }
private shouldRestoreFocus(expectedActiveElement: Element | null): boolean {
if (!this.layoutService.isRestored()) {
return true; // restore focus if we are not restored yet on startup
}
if (!expectedActiveElement) {
return true; // restore focus if nothing was focused
}
const activeElement = document.activeElement;
if (!activeElement || activeElement === document.body) {
return true; // restore focus if nothing is focused currently
}
const same = expectedActiveElement === activeElement;
if (same) {
return true; // restore focus if same element is still active
}
if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA') {
// This is to avoid regressions from not restoring focus as we used to:
// Only allow a different input element (or textarea) to remain focused
// but not other elements that do not accept text input.
return true;
}
if (isAncestor(activeElement, this.editorGroupParent)) {
return true; // restore focus if active element is still inside our editor group
}
return false; // do not restore focus
}
private getEditorPaneDescriptor(editor: EditorInput): IEditorPaneDescriptor { private getEditorPaneDescriptor(editor: EditorInput): IEditorPaneDescriptor {
if (editor.hasCapability(EditorInputCapabilities.RequiresTrust) && !this.workspaceTrustService.isWorkspaceTrusted()) { if (editor.hasCapability(EditorInputCapabilities.RequiresTrust) && !this.workspaceTrustService.isWorkspaceTrusted()) {
// Workspace trust: if an editor signals it needs workspace trust // Workspace trust: if an editor signals it needs workspace trust
@ -281,7 +319,7 @@ export class EditorPanes extends Disposable {
// Show editor // Show editor
const container = assertIsDefined(editorPane.getContainer()); const container = assertIsDefined(editorPane.getContainer());
this.parent.appendChild(container); this.editorPanesParent.appendChild(container);
show(container); show(container);
// Indicate to editor that it is now visible // Indicate to editor that it is now visible
@ -403,7 +441,7 @@ export class EditorPanes extends Disposable {
// Remove editor pane from parent // Remove editor pane from parent
const editorPaneContainer = this._activeEditorPane.getContainer(); const editorPaneContainer = this._activeEditorPane.getContainer();
if (editorPaneContainer) { if (editorPaneContainer) {
this.parent.removeChild(editorPaneContainer); this.editorPanesParent.removeChild(editorPaneContainer);
hide(editorPaneContainer); hide(editorPaneContainer);
} }