diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index e32589a6800..2e116ac844b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -71,9 +71,7 @@ suite('vscode API - window', () => { reg.dispose(); }); - test('editor, onDidChangeTextEditorViewColumn (close editor)', () => { - - let actualEvent: TextEditorViewColumnChangeEvent; + test('editor, onDidChangeTextEditorViewColumn (close editor)', async () => { const registration1 = workspace.registerTextDocumentContentProvider('bikes', { provideTextDocumentContent() { @@ -81,33 +79,30 @@ suite('vscode API - window', () => { } }); - return Promise.all([ - workspace.openTextDocument(Uri.parse('bikes://testing/one')).then(doc => window.showTextDocument(doc, ViewColumn.One)), - workspace.openTextDocument(Uri.parse('bikes://testing/two')).then(doc => window.showTextDocument(doc, ViewColumn.Two)) - ]).then(async editors => { + const doc1 = await workspace.openTextDocument(Uri.parse('bikes://testing/one')); + await window.showTextDocument(doc1, ViewColumn.One); - 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(resolve => { - const registration2 = window.onDidChangeTextEditorViewColumn(event => { - actualEvent = event; - registration2.dispose(); - resolve(); - }); - // close editor 1, wait a little for the event to bubble - one.hide(); + assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.Two); + + const actualEvent = await new Promise(resolve => { + const registration2 = window.onDidChangeTextEditorViewColumn(event => { + registration2.dispose(); + resolve(event); }); - assert.ok(actualEvent); - assert.ok(actualEvent.textEditor === two); - assert.ok(actualEvent.viewColumn === two.viewColumn); - - registration1.dispose(); + // close editor 1, wait a little for the event to bubble + commands.executeCommand('workbench.action.closeEditorsInOtherGroups'); }); + assert.ok(actualEvent); + assert.ok(actualEvent.textEditor === two); + assert.ok(actualEvent.viewColumn === two.viewColumn); + + registration1.dispose(); }); - test('editor, onDidChangeTextEditorViewColumn (move editor group)', () => { - - const actualEvents: TextEditorViewColumnChangeEvent[] = []; + test('editor, onDidChangeTextEditorViewColumn (move editor group)', async () => { const registration1 = workspace.registerTextDocumentContentProvider('bikes', { provideTextDocumentContent() { @@ -115,38 +110,38 @@ suite('vscode API - window', () => { } }); - return Promise.all([ - workspace.openTextDocument(Uri.parse('bikes://testing/one')).then(doc => window.showTextDocument(doc, ViewColumn.One)), - workspace.openTextDocument(Uri.parse('bikes://testing/two')).then(doc => window.showTextDocument(doc, ViewColumn.Two)) - ]).then(editors => { + const doc1 = await workspace.openTextDocument(Uri.parse('bikes://testing/one')); + await window.showTextDocument(doc1, ViewColumn.One); - const [, two] = editors; - two.show(); + const doc2 = await workspace.openTextDocument(Uri.parse('bikes://testing/two')); + await window.showTextDocument(doc2, ViewColumn.Two); - return new Promise(resolve => { + assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.Two); - const registration2 = window.onDidChangeTextEditorViewColumn(event => { - actualEvents.push(event); + const actualEvents = await new Promise(resolve => { - if (actualEvents.length === 2) { - registration2.dispose(); - resolve(); - } - }); + const actualEvents: TextEditorViewColumnChangeEvent[] = []; - // move active editor group left - return commands.executeCommand('workbench.action.moveActiveEditorGroupLeft'); + const registration2 = window.onDidChangeTextEditorViewColumn(event => { + actualEvents.push(event); - }).then(() => { - assert.strictEqual(actualEvents.length, 2); - - for (const event of actualEvents) { - assert.strictEqual(event.viewColumn, event.textEditor.viewColumn); + if (actualEvents.length === 2) { + registration2.dispose(); + resolve(actualEvents); } - - 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 () { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 84656977f8d..1d5d4963cab 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -210,7 +210,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(this.editorContainer); // 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; // Track Focus @@ -511,6 +511,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // not changed meanwhile. This prevents focus from being // stolen accidentally on startup when the user already // clicked somewhere. + if (this.accessor.activeGroup === this && activeElement === document.activeElement) { this.focus(); } diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index fd9dddc21ef..a7975e96f54 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, isEditorOpenError } from 'vs/workbench/common/editor'; 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 { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -91,7 +91,8 @@ export class EditorPanes extends Disposable { private readonly editorPanesRegistry = Registry.as(EditorExtensions.EditorPane); constructor( - private parent: HTMLElement, + private editorGroupParent: HTMLElement, + private editorPanesParent: HTMLElement, private groupView: IEditorGroupView, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -237,20 +238,57 @@ export class EditorPanes extends Disposable { // Editor pane const pane = this.doShowEditorPane(descriptor); + // Remember current active element for deciding to restore focus later + const activeElement = document.activeElement; + // Apply input to pane const { changed, cancelled } = await this.doSetInput(pane, editor, options, context); - // Focus unless cancelled - if (!cancelled) { - const focus = !options || !options.preserveFocus; - if (focus) { - pane.focus(); - } + // Focus only if not cancelled and not prevented + const focus = !options || !options.preserveFocus; + if (!cancelled && focus && this.shouldRestoreFocus(activeElement)) { + pane.focus(); } 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 { if (editor.hasCapability(EditorInputCapabilities.RequiresTrust) && !this.workspaceTrustService.isWorkspaceTrusted()) { // Workspace trust: if an editor signals it needs workspace trust @@ -281,7 +319,7 @@ export class EditorPanes extends Disposable { // Show editor const container = assertIsDefined(editorPane.getContainer()); - this.parent.appendChild(container); + this.editorPanesParent.appendChild(container); show(container); // Indicate to editor that it is now visible @@ -403,7 +441,7 @@ export class EditorPanes extends Disposable { // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); if (editorPaneContainer) { - this.parent.removeChild(editorPaneContainer); + this.editorPanesParent.removeChild(editorPaneContainer); hide(editorPaneContainer); }