On the fly tab model construction (#133025)

* Enrich the change event

* Initial tab model building

* Work in progress model construction

* Add pauseable emitter

* Attempt using microtask

* Make tests pass

* Update active tab logic

* Fix layering issue

* event rename

* PR feedback

* Remove stray new line

* Add test for microtask emitter

* Add move event

* Add mmerge functionality to Microtask emitter

* Fix compilation errrors

* Fix tests

* Add tests to address feedback

* Change editor change event to an array

* Add array support to editorsChangeEvent

* Update src/vs/workbench/common/editor/editorGroupModel.ts

Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>

* Switch to a less efficient array method

* Add console.log for debugging

* Test with different notebook

* Fix notebook URI

* For now split up file open for better debugging

* Don't use notebook in test for now

* Cleanup event

* Fix tests

Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>
This commit is contained in:
Logan Ramos 2021-09-16 14:34:54 -04:00 committed by GitHub
parent c78811c1a7
commit 76c3eb94c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 345 additions and 96 deletions

View file

@ -49,7 +49,8 @@ const CORE_TYPES = [
'decode',
'self',
'trimLeft',
'trimRight'
'trimRight',
'queueMicrotask'
];
// Types that are defined in a common layer but are known to be only
// available in native environments should not be allowed in browser

View file

@ -50,7 +50,8 @@ const CORE_TYPES = [
'decode',
'self',
'trimLeft',
'trimRight'
'trimRight',
'queueMicrotask'
];
// Types that are defined in a common layer but are known to be only

View file

@ -348,33 +348,30 @@ suite('vscode API - window', () => {
});
//#region Tabs API tests
test.skip('Tabs - Ensure tabs getter is correct', async () => {
assert.ok(workspace.workspaceFolders);
const workspaceRoot = workspace.workspaceFolders[0].uri;
const [docA, docB, docC, notebookDoc] = await Promise.all([
workspace.openTextDocument(await createRandomFile()),
workspace.openTextDocument(await createRandomFile()),
workspace.openTextDocument(await createRandomFile()),
workspace.openNotebookDocument(Uri.joinPath(workspaceRoot, 'test.ipynb'))
]);
test('Tabs - Ensure tabs getter is correct', async () => {
const docA = await workspace.openTextDocument(await createRandomFile());
const docB = await workspace.openTextDocument(await createRandomFile());
const docC = await workspace.openTextDocument(await createRandomFile());
// Add back actual notebook doc once stuck promise is figured out
//const notebookDoc = await workspace.openNotebookDocument(await createRandomFile('', undefined, '.vsctestnb'));
const notebookDoc = await workspace.openTextDocument(await createRandomFile());
// const [docA, docB, docC, notebookDoc] = await Promise.all([
// workspace.openTextDocument(await createRandomFile()),
// workspace.openTextDocument(await createRandomFile()),
// workspace.openTextDocument(await createRandomFile()),
// workspace.openNotebookDocument(await createRandomFile('', undefined, '.vsctestnb'))
// ]);
await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false });
await window.showTextDocument(docB, { viewColumn: ViewColumn.Two, preview: false });
await window.showTextDocument(docC, { viewColumn: ViewColumn.Three, preview: false });
await window.showNotebookDocument(notebookDoc, { viewColumn: ViewColumn.One, preview: false });
await window.showTextDocument(notebookDoc, { viewColumn: ViewColumn.One, preview: false });
//await window.showNotebookDocument(notebookDoc, { viewColumn: ViewColumn.One, preview: false });
const leftDiff = await createRandomFile();
const rightDiff = await createRandomFile();
await commands.executeCommand('vscode.diff', leftDiff, rightDiff, 'Diff', { viewColumn: ViewColumn.Three, preview: false });
// Wait for the tab change event to fire
await new Promise<void>((resolve) => {
const dispsable = window.onDidChangeTabs(() => {
dispsable.dispose();
resolve();
});
});
const tabs = window.tabs;
assert.strictEqual(tabs.length, 5);
@ -393,16 +390,8 @@ suite('vscode API - window', () => {
assert.strictEqual(tabs[4].viewColumn, ViewColumn.Three);
});
test.skip('Tabs - ensure active tab is correct', async () => {
test('Tabs - ensure active tab is correct', async () => {
function createActiveTabListenerPromise(): Promise<void> {
return new Promise<void>((resolve) => {
const dispsable = window.onDidChangeActiveTab(() => {
dispsable.dispose();
resolve();
});
});
}
const [docA, docB, docC] = await Promise.all([
workspace.openTextDocument(await createRandomFile()),
workspace.openTextDocument(await createRandomFile()),
@ -410,24 +399,20 @@ suite('vscode API - window', () => {
]);
await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false });
await createActiveTabListenerPromise();
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docA.uri.toString());
await window.showTextDocument(docB, { viewColumn: ViewColumn.Two, preview: false });
await createActiveTabListenerPromise();
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docB.uri.toString());
await window.showTextDocument(docC, { viewColumn: ViewColumn.Three, preview: false });
await createActiveTabListenerPromise();
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docC.uri.toString());
await commands.executeCommand('workbench.action.closeActiveEditor');
await commands.executeCommand('workbench.action.closeActiveEditor');
await commands.executeCommand('workbench.action.closeActiveEditor');
await createActiveTabListenerPromise();
assert.ok(!window.activeTab);

View file

@ -732,6 +732,33 @@ export class DebounceEmitter<T> extends PauseableEmitter<T> {
}
}
/**
* An emitter which queue all events and then process them at the
* end of the event loop.
*/
export class MicrotaskEmitter<T> extends Emitter<T> {
private _queuedEvents: T[] = [];
private _mergeFn?: (input: T[]) => T;
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options?.merge;
}
override fire(event: T): void {
this._queuedEvents.push(event);
if (this._queuedEvents.length === 1) {
queueMicrotask(() => {
if (this._mergeFn) {
super.fire(this._mergeFn(this._queuedEvents));
} else {
this._queuedEvents.forEach(e => super.fire(e));
}
this._queuedEvents = [];
});
}
}
}
export class EventMultiplexer<T> implements IDisposable {
private readonly emitter: Emitter<T>;

View file

@ -6,7 +6,7 @@ import * as assert from 'assert';
import { timeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { AsyncEmitter, DebounceEmitter, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, PauseableEmitter, Relay } from 'vs/base/common/event';
import { AsyncEmitter, DebounceEmitter, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay } from 'vs/base/common/event';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
namespace Samples {
@ -274,6 +274,29 @@ suite('Event', function () {
assert.strictEqual(sum, 3);
});
test('Microtask Emitter', (done) => {
let count = 0;
assert.strictEqual(count, 0);
const emitter = new MicrotaskEmitter<void>();
const listener = emitter.event(() => {
count++;
});
emitter.fire();
assert.strictEqual(count, 0);
emitter.fire();
assert.strictEqual(count, 0);
// Should wait until the event loop ends and therefore be the last thing called
setTimeout(() => {
assert.strictEqual(count, 3);
done();
}, 0);
queueMicrotask(() => {
assert.strictEqual(count, 2);
count++;
listener.dispose();
});
});
test('Emitter - In Order Delivery', function () {
const a = new Emitter<string>();
const listener2Events: string[] = [];

View file

@ -4,12 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor';
import { EditorResourceAccessor, IEditorsChangeEvent, SideBySideEditor } from 'vs/workbench/common/editor';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { GroupChangeKind, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@ -18,6 +19,9 @@ export class MainThreadEditorTabs {
private readonly _dispoables = new DisposableStore();
private readonly _proxy: IExtHostEditorTabsShape;
private readonly _tabModel: Map<number, IEditorTabDto[]> = new Map<number, IEditorTabDto[]>();
private _currentlyActiveTab: { groupId: number, tab: IEditorTabDto } | undefined = undefined;
private _oldTabModel: IEditorTabDto[] = [];
constructor(
extHostContext: IExtHostContext,
@ -27,31 +31,171 @@ export class MainThreadEditorTabs {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);
this._dispoables.add(editorService.onDidEditorsChange(this._pushEditorTabs, this));
this._editorGroupsService.whenReady.then(() => this._pushEditorTabs());
// Queue all events that arrive on the same event loop and then send them as a batch
this._dispoables.add(editorService.onDidEditorsChange((events) => this._updateTabsModel(events)));
this._editorGroupsService.whenReady.then(() => this._createTabsModel());
}
dispose(): void {
this._dispoables.dispose();
}
private _pushEditorTabs(): void {
const tabs: IEditorTabDto[] = [];
private _createTabsModel(): void {
this._tabModel.clear();
let tabs: IEditorTabDto[] = [];
for (const group of this._editorGroupsService.groups) {
for (const editor of group.editors) {
if (editor.isDisposed()) {
continue;
}
tabs.push({
const tab = {
viewColumn: editorGroupToColumn(this._editorGroupsService, group),
label: editor.getName(),
resource: editor instanceof SideBySideEditorInput ? EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }) : EditorResourceAccessor.getCanonicalUri(editor),
editorId: editor.editorId,
isActive: (this._editorGroupsService.activeGroup === group) && group.isActive(editor)
});
};
if (tab.isActive) {
this._currentlyActiveTab = { groupId: group.id, tab };
}
tabs.push(tab);
}
this._tabModel.set(group.id, tabs);
}
this._proxy.$acceptEditorTabs(tabs);
}
private _onDidTabOpen(event: IEditorsChangeEvent): void {
if (event.kind !== GroupChangeKind.EDITOR_OPEN || !event.editor || event.editorIndex === undefined) {
return;
}
if (!this._tabModel.has(event.groupId)) {
this._tabModel.set(event.groupId, []);
}
const editor = event.editor;
const tab = {
viewColumn: editorGroupToColumn(this._editorGroupsService, event.groupId),
label: editor.getName(),
resource: editor instanceof SideBySideEditorInput ? EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }) : EditorResourceAccessor.getCanonicalUri(editor),
editorId: editor.editorId,
isActive: (this._editorGroupsService.activeGroup.id === event.groupId) && this._editorGroupsService.activeGroup.isActive(editor)
};
this._tabModel.get(event.groupId)?.splice(event.editorIndex, 0, tab);
// Update the currently active tab which may or may not be the opened one
if (tab.isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive({ resource: URI.revive(this._currentlyActiveTab.tab.resource), options: { override: this._currentlyActiveTab.tab.editorId } });
}
this._currentlyActiveTab = { groupId: event.groupId, tab };
}
}
private _onDidTabClose(event: IEditorsChangeEvent): void {
if (event.kind !== GroupChangeKind.EDITOR_CLOSE || event.editorIndex === undefined) {
return;
}
this._tabModel.get(event.groupId)?.splice(event.editorIndex, 1);
this._findAndUpdateActiveTab();
// Remove any empty groups
if (this._tabModel.get(event.groupId)?.length === 0) {
this._tabModel.delete(event.groupId);
}
}
private _onDidTabMove(event: IEditorsChangeEvent): void {
if (event.kind !== GroupChangeKind.EDITOR_MOVE || event.editorIndex === undefined || event.previousEditorIndex === undefined) {
return;
}
const movedTab = this._tabModel.get(event.groupId)?.splice(event.previousEditorIndex, 1);
if (movedTab === undefined) {
return;
}
this._tabModel.get(event.groupId)?.splice(event.previousEditorIndex, 0, movedTab[0]);
movedTab[0].isActive = (this._editorGroupsService.activeGroup.id === event.groupId) && this._editorGroupsService.activeGroup.isActive({ resource: URI.revive(movedTab[0].resource), options: { override: movedTab[0].editorId } });
// Update the currently active tab
if (movedTab[0].isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive({ resource: URI.revive(this._currentlyActiveTab.tab.resource), options: { override: this._currentlyActiveTab.tab.editorId } });
}
this._currentlyActiveTab = { groupId: event.groupId, tab: movedTab[0] };
}
}
private _onDidGroupActivate(event: IEditorsChangeEvent): void {
if (event.kind !== GroupChangeKind.GROUP_INDEX) {
return;
}
this._findAndUpdateActiveTab();
}
private _findAndUpdateActiveTab() {
// Go to the active group and update the active tab
const activeGroupId = this._editorGroupsService.activeGroup.id;
this._tabModel.get(activeGroupId)?.forEach(t => {
if (t.resource) {
t.isActive = this._editorGroupsService.activeGroup.isActive({ resource: URI.revive(t.resource), options: { override: t.editorId } });
}
if (t.isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive({ resource: URI.revive(this._currentlyActiveTab.tab.resource), options: { override: this._currentlyActiveTab.tab.editorId } });
}
this._currentlyActiveTab = { groupId: activeGroupId, tab: t };
return;
}
}, this);
}
/**
* Helper function to compare previous tab model to current one
* @param current The current tab model to compare to the previous mode
* @returns True if they're equivalent, false otherwise
*/
private _compareTabsModel(current: IEditorTabDto[]): boolean {
if (this._oldTabModel.length !== current.length) {
return false;
}
for (let i = 0; i < current.length; i++) {
if (this._oldTabModel[i].resource !== current[i].resource && this._oldTabModel[i].editorId !== current[i].editorId) {
return false;
}
}
return true;
}
private _updateTabsModel(events: IEditorsChangeEvent[]): void {
events.forEach(event => {
// Call the correct function for the change type
switch (event.kind) {
case GroupChangeKind.EDITOR_OPEN:
this._onDidTabOpen(event);
break;
case GroupChangeKind.EDITOR_CLOSE:
this._onDidTabClose(event);
break;
case GroupChangeKind.GROUP_ACTIVE:
if (this._editorGroupsService.activeGroup.id !== event.groupId) {
return;
}
this._onDidGroupActivate(event);
break;
case GroupChangeKind.GROUP_INDEX:
this._createTabsModel();
// Here we stop the loop as no need to process other events
break;
case GroupChangeKind.EDITOR_MOVE:
this._onDidTabMove(event);
break;
default:
break;
}
});
// Flatten the map into a singular array to send the ext host
let allTabs: IEditorTabDto[] = [];
this._tabModel.forEach((tabs) => allTabs = allTabs.concat(tabs));
if (!this._compareTabsModel(allTabs)) {
this._proxy.$acceptEditorTabs(allTabs);
this._oldTabModel = allTabs;
}
}
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/editorgroupview';
import { EditorGroupModel, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroupModel, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel';
import { EditorGroupModel, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroupModel, isSerializedEditorGroupModel, EditorMoveEvent, EditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel';
import { GroupIdentifier, CloseDirection, IEditorCloseEvent, ActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, ActiveEditorStickyContext, ActiveEditorPinnedContext, EditorResourceAccessor, IEditorMoveEvent, EditorInputCapabilities, IEditorOpenEvent, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, ActiveEditorGroupLockedContext, SideBySideEditor, EditorCloseContext } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
@ -525,6 +525,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this._register(this.model.onDidChangeLocked(() => this.onDidChangeGroupLocked()));
this._register(this.model.onDidChangeEditorPinned(editor => this.onDidChangeEditorPinned(editor)));
this._register(this.model.onDidChangeEditorSticky(editor => this.onDidChangeEditorSticky(editor)));
this._register(this.model.onDidMoveEditor(event => this.onDidMoveEditor(event)));
this._register(this.model.onDidOpenEditor(editor => this.onDidOpenEditor(editor)));
this._register(this.model.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor)));
this._register(this.model.onWillDisposeEditor(editor => this.onWillDisposeEditor(editor)));
@ -551,7 +552,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_STICKY, editor });
}
private onDidOpenEditor(editor: EditorInput): void {
private onDidMoveEditor(event: EditorMoveEvent): void {
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_MOVE, editor: event.editor, previousEditorIndex: event.index, editorIndex: event.newIndex });
}
private onDidOpenEditor({ editor, index }: EditorOpenEvent): void {
/* __GDPR__
"editorOpened" : {
@ -566,7 +571,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.updateContainer();
// Event
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_OPEN, editor });
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_OPEN, editor, editorIndex: index });
}
private handleOnDidCloseEditor(event: EditorCloseEvent): void {
@ -929,9 +934,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
const newIndexOfEditor = this.getIndexOfEditor(editor);
if (newIndexOfEditor !== oldIndexOfEditor) {
this.titleAreaControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor);
// Event
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_MOVE, editor });
}
// Forward sticky state to title control
@ -1290,9 +1292,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Forward to title area
this.titleAreaControl.moveEditor(editor, currentIndex, moveToIndex);
this.titleAreaControl.pinEditor(editor);
// Event
this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_MOVE, editor });
}
private doMoveOrCopyEditorAcrossGroups(editor: EditorInput, target: EditorGroupView, openOptions?: IEditorOpenOptions, internalOptions?: IInternalMoveCopyOptions): void {

View file

@ -15,7 +15,7 @@ import { IInstantiationService, IConstructorSignature0, ServicesAccessor, Brande
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Registry } from 'vs/platform/registry/common/platform';
import { IEncodingSupport, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroup, IGroupChangeEvent } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ICompositeControl, IComposite } from 'vs/workbench/common/composite';
import { IFileService } from 'vs/platform/files/common/files';
import { IPathData } from 'vs/platform/windows/common/windows';
@ -763,6 +763,10 @@ export interface IEditorCloseEvent extends IEditorIdentifier {
sticky: boolean;
}
export interface IEditorsChangeEvent extends IGroupChangeEvent {
groupId: GroupIdentifier;
}
export interface IEditorMoveEvent extends IEditorIdentifier {
target: GroupIdentifier;
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { IEditorFactoryRegistry, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, EditorsOrder, EditorExtensions, IUntypedEditorInput, SideBySideEditor, EditorCloseContext } from 'vs/workbench/common/editor';
import { IEditorFactoryRegistry, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, EditorsOrder, EditorExtensions, IUntypedEditorInput, SideBySideEditor, IEditorMoveEvent, IEditorOpenEvent, EditorCloseContext } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@ -24,6 +24,17 @@ export interface EditorCloseEvent extends IEditorCloseEvent {
readonly editor: EditorInput;
}
export interface EditorOpenEvent extends IEditorOpenEvent {
readonly editor: EditorInput;
readonly index: number;
}
export interface EditorMoveEvent extends IEditorMoveEvent {
readonly editor: EditorInput;
readonly index: number;
readonly newIndex: number;
}
export interface EditorIdentifier extends IEditorIdentifier {
readonly groupId: GroupIdentifier;
readonly editor: EditorInput;
@ -91,7 +102,7 @@ export class EditorGroupModel extends Disposable {
private readonly _onDidActivateEditor = this._register(new Emitter<EditorInput>());
readonly onDidActivateEditor = this._onDidActivateEditor.event;
private readonly _onDidOpenEditor = this._register(new Emitter<EditorInput>());
private readonly _onDidOpenEditor = this._register(new Emitter<EditorOpenEvent>());
readonly onDidOpenEditor = this._onDidOpenEditor.event;
private readonly _onDidCloseEditor = this._register(new Emitter<EditorCloseEvent>());
@ -109,7 +120,7 @@ export class EditorGroupModel extends Disposable {
private readonly _onDidChangeEditorCapabilities = this._register(new Emitter<EditorInput>());
readonly onDidChangeEditorCapabilities = this._onDidChangeEditorCapabilities.event;
private readonly _onDidMoveEditor = this._register(new Emitter<EditorInput>());
private readonly _onDidMoveEditor = this._register(new Emitter<EditorMoveEvent>());
readonly onDidMoveEditor = this._onDidMoveEditor.event;
private readonly _onDidChangeEditorPinned = this._register(new Emitter<EditorInput>());
@ -295,7 +306,7 @@ export class EditorGroupModel extends Disposable {
this.registerEditorListeners(newEditor);
// Event
this._onDidOpenEditor.fire(newEditor);
this._onDidOpenEditor.fire({ editor: newEditor, groupId: this.id, index: targetIndex });
// Handle active
if (makeActive) {
@ -475,7 +486,7 @@ export class EditorGroupModel extends Disposable {
this.editors.splice(toIndex, 0, editor);
// Event
this._onDidMoveEditor.fire(editor);
this._onDidMoveEditor.fire({ editor, groupId: this.id, index, newIndex: toIndex, target: this.id });
return editor;
}

View file

@ -5,12 +5,12 @@
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IResourceEditorInput, IEditorOptions, EditorActivation, EditorResolution, IResourceEditorInputIdentifier, ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor';
import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, IEditorsChangeEvent } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { ResourceMap } from 'vs/base/common/map';
import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { Event, Emitter, DebounceEmitter } from 'vs/base/common/event';
import { Event, Emitter, MicrotaskEmitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
@ -47,7 +47,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
private readonly _onDidEditorsChange = this._register(new DebounceEmitter<void>({ delay: 0, merge: () => undefined }));
private readonly _onDidEditorsChange = this._register(new MicrotaskEmitter<IEditorsChangeEvent[]>({ merge: (events) => events.flat(1) }));
readonly onDidEditorsChange = this._onDidEditorsChange.event;
private readonly _onDidCloseEditor = this._register(new Emitter<IEditorCloseEvent>());
@ -87,7 +87,6 @@ export class EditorService extends Disposable implements EditorServiceImpl {
this.editorGroupService.whenReady.then(() => this.onEditorGroupsReady());
this.editorGroupService.onDidChangeActiveGroup(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
this.editorGroupService.onDidMoveGroup(group => this.handleGroupMove(group));
this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
// Out of workspace file watchers
@ -123,14 +122,6 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
}
private handleGroupMove(group: IEditorGroup): void {
if (group.isEmpty) {
return; // empty groups do not change structure of editors
}
this._onDidEditorsChange.fire();
}
private handleActiveEditorChange(group: IEditorGroup): void {
if (group !== this.editorGroupService.activeGroup) {
return; // ignore if not the active group
@ -151,7 +142,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Fire event to outside parties
this._onDidActiveEditorChange.fire();
this._onDidEditorsChange.fire();
this._onDidEditorsChange.fire([{ groupId: activeGroup.id, editor: this.lastActiveEditor, kind: GroupChangeKind.EDITOR_ACTIVE }]);
}
private registerGroupListeners(group: IEditorGroupView): void {
@ -163,10 +154,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
this.handleActiveEditorChange(group);
this._onDidVisibleEditorsChange.fire();
break;
case GroupChangeKind.EDITOR_CLOSE:
case GroupChangeKind.EDITOR_OPEN:
case GroupChangeKind.EDITOR_MOVE:
this._onDidEditorsChange.fire();
default:
this._onDidEditorsChange.fire([{ groupId: group.id, ...e }]);
break;
}
}));

View file

@ -418,6 +418,11 @@ export interface IGroupChangeEvent {
kind: GroupChangeKind;
editor?: EditorInput;
editorIndex?: number;
/**
* For EDITOR_MOVE only. Signifies the index the editor is moving from.
* editorIndex will contain the index the editor is moving to.
*/
previousEditorIndex?: number;
}
export const enum OpenEditorContext {

View file

@ -5,7 +5,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IResourceEditorInput, IEditorOptions, IResourceEditorInputIdentifier, ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, IEditorsChangeEvent } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { Event } from 'vs/base/common/event';
import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon';
@ -105,7 +105,7 @@ export interface IEditorService {
* - editors moving
* - groups moving (unless they are empty)
*/
readonly onDidEditorsChange: Event<void>;
readonly onDidEditorsChange: Event<IEditorsChangeEvent[]>;
/**
* Emitted when an editor is closed.

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { EditorGroupModel, ISerializedEditorGroupModel, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroupModel';
import { EditorGroupModel, ISerializedEditorGroupModel, EditorCloseEvent, EditorMoveEvent, EditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel';
import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, CloseDirection, EditorsOrder, IResourceDiffEditorInput, IResourceSideBySideEditorInput, SideBySideEditor, EditorCloseContext } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { TestLifecycleService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
@ -80,14 +80,14 @@ suite('EditorGroupModel', () => {
interface GroupEvents {
locked: number[],
opened: EditorInput[];
opened: EditorOpenEvent[];
activated: EditorInput[];
closed: EditorCloseEvent[];
pinned: EditorInput[];
unpinned: EditorInput[];
sticky: EditorInput[];
unsticky: EditorInput[];
moved: EditorInput[];
moved: EditorMoveEvent[];
disposed: EditorInput[];
}
@ -746,7 +746,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isPinned(input1), true);
assert.strictEqual(group.isPinned(0), true);
assert.strictEqual(events.opened[0], input1);
assert.strictEqual(events.opened[0].editor, input1);
assert.strictEqual(events.activated[0], input1);
let index = group.indexOf(input1);
@ -771,7 +771,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isPinned(input2), false);
assert.strictEqual(group.isPinned(0), false);
assert.strictEqual(events.opened[1], input2);
assert.strictEqual(events.opened[1].editor, input2);
assert.strictEqual(events.activated[1], input2);
group.closeEditor(input2);
@ -800,7 +800,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isPinned(input3), true);
assert.strictEqual(group.isPinned(0), true);
assert.strictEqual(events.opened[2], input3);
assert.strictEqual(events.opened[2].editor, input3);
assert.strictEqual(events.activated[2], input3);
group.closeEditor(input3);
@ -809,7 +809,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.activeEditor, null);
assert.strictEqual(events.closed[2].editor, input3);
assert.strictEqual(events.opened[2], input3);
assert.strictEqual(events.opened[2].editor, input3);
assert.strictEqual(events.activated[2], input3);
group.closeEditor(input3);
@ -829,7 +829,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isPinned(input4), false);
assert.strictEqual(group.isPinned(0), false);
assert.strictEqual(events.opened[3], input4);
assert.strictEqual(events.opened[3].editor, input4);
assert.strictEqual(events.activated[3], input4);
group.closeEditor(input4);
@ -870,9 +870,9 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isActive(input3), true);
assert.strictEqual(group.isPinned(input3), true);
assert.strictEqual(events.opened[0], input1);
assert.strictEqual(events.opened[1], input2);
assert.strictEqual(events.opened[2], input3);
assert.strictEqual(events.opened[0].editor, input1);
assert.strictEqual(events.opened[1].editor, input2);
assert.strictEqual(events.opened[2].editor, input3);
assert.strictEqual(events.activated[0], input1);
assert.strictEqual(events.activated[1], input2);
@ -902,7 +902,7 @@ suite('EditorGroupModel', () => {
assert.strictEqual(events.unsticky[0], input1);
group.moveEditor(sameInput1, 1);
assert.strictEqual(events.moved[0], input1);
assert.strictEqual(events.moved[0].editor, input1);
group.closeEditor(sameInput1);
assert.strictEqual(events.closed[0].editor, input1);
@ -1018,9 +1018,9 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.isPinned(input3), false);
assert.strictEqual(!group.isPinned(input3), true);
assert.strictEqual(events.opened[0], input1);
assert.strictEqual(events.opened[1], input2);
assert.strictEqual(events.opened[2], input3);
assert.strictEqual(events.opened[0].editor, input1);
assert.strictEqual(events.opened[1].editor, input2);
assert.strictEqual(events.opened[2].editor, input3);
assert.strictEqual(events.closed[0].editor, input1);
assert.strictEqual(events.closed[1].editor, input2);
assert.strictEqual(events.closed[0].context === EditorCloseContext.REPLACE, true);
@ -1237,7 +1237,7 @@ suite('EditorGroupModel', () => {
group.moveEditor(input1, 1);
assert.strictEqual(events.moved[0], input1);
assert.strictEqual(events.moved[0].editor, input1);
assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input2);
assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input1);
@ -1248,7 +1248,7 @@ suite('EditorGroupModel', () => {
group.moveEditor(input4, 0);
assert.strictEqual(events.moved[1], input4);
assert.strictEqual(events.moved[1].editor, input4);
assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input4);
assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2);
assert.strictEqual(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input1);
@ -2117,4 +2117,64 @@ suite('EditorGroupModel', () => {
assert.strictEqual(group.indexOf(input3), 3);
assert.strictEqual(group.indexOf(input4), 2);
});
test('onDidMoveEditor Event', () => {
const group1 = createEditorGroupModel();
const group2 = createEditorGroupModel();
const input1group1 = input();
const input2group1 = input();
const input1group2 = input();
const input2group2 = input();
// Open all the editors
group1.openEditor(input1group1, { pinned: true, active: true, index: 0 });
group1.openEditor(input2group1, { pinned: true, active: false, index: 1 });
group2.openEditor(input1group2, { pinned: true, active: true, index: 0 });
group2.openEditor(input2group2, { pinned: true, active: false, index: 1 });
const group1Events = groupListener(group1);
const group2Events = groupListener(group2);
group1.moveEditor(input1group1, 1);
assert.strictEqual(group1Events.moved[0].editor, input1group1);
assert.strictEqual(group1Events.moved[0].index, 0);
assert.strictEqual(group1Events.moved[0].newIndex, 1);
group2.moveEditor(input1group2, 1);
assert.strictEqual(group2Events.moved[0].editor, input1group2);
assert.strictEqual(group2Events.moved[0].index, 0);
assert.strictEqual(group2Events.moved[0].newIndex, 1);
});
test('onDidOpeneditor Event', () => {
const group1 = createEditorGroupModel();
const group2 = createEditorGroupModel();
const group1Events = groupListener(group1);
const group2Events = groupListener(group2);
const input1group1 = input();
const input2group1 = input();
const input1group2 = input();
const input2group2 = input();
// Open all the editors
group1.openEditor(input1group1, { pinned: true, active: true, index: 0 });
group1.openEditor(input2group1, { pinned: true, active: false, index: 1 });
group2.openEditor(input1group2, { pinned: true, active: true, index: 0 });
group2.openEditor(input2group2, { pinned: true, active: false, index: 1 });
assert.strictEqual(group1Events.opened.length, 2);
assert.strictEqual(group1Events.opened[0].editor, input1group1);
assert.strictEqual(group1Events.opened[0].index, 0);
assert.strictEqual(group1Events.opened[1].editor, input2group1);
assert.strictEqual(group1Events.opened[1].index, 1);
assert.strictEqual(group2Events.opened.length, 2);
assert.strictEqual(group2Events.opened[0].editor, input1group2);
assert.strictEqual(group2Events.opened[0].index, 0);
assert.strictEqual(group2Events.opened[1].editor, input2group2);
assert.strictEqual(group2Events.opened[1].index, 1);
});
});

View file

@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorFactoryRegistry, IEditorSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, IEditorMoveEvent, EditorExtensions as Extensions, EditorInputCapabilities, IEditorOpenEvent, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorFactoryRegistry, IEditorSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, IEditorMoveEvent, EditorExtensions as Extensions, EditorInputCapabilities, IEditorOpenEvent, IUntypedEditorInput, IEditorsChangeEvent } from 'vs/workbench/common/editor';
import { EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
import { IResolvedWorkingCopyBackup, IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup';
@ -805,7 +805,7 @@ export class TestEditorService implements EditorServiceImpl {
onDidActiveEditorChange: Event<void> = Event.None;
onDidVisibleEditorsChange: Event<void> = Event.None;
onDidEditorsChange: Event<void> = Event.None;
onDidEditorsChange: Event<IEditorsChangeEvent[]> = Event.None;
onDidCloseEditor: Event<IEditorCloseEvent> = Event.None;
onDidOpenEditorFail: Event<IEditorIdentifier> = Event.None;
onDidMostRecentlyActiveEditorsChange: Event<void> = Event.None;