Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54 (#8757)

* Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54

* disable failing tests
This commit is contained in:
Anthony Dresser
2019-12-19 23:41:55 -08:00
committed by GitHub
parent 778a34a9d2
commit 74caccdfe6
40 changed files with 2058 additions and 1299 deletions

View File

@@ -151,4 +151,14 @@ export interface EditorServiceImpl extends IEditorService {
* Emitted when an editor failed to open.
*/
readonly onDidOpenEditorFail: Event<IEditorIdentifier>;
/**
* Emitted when the list of most recently active editors change.
*/
readonly onDidMostRecentlyActiveEditorsChange: Event<void>;
/**
* Access to the list of most recently active editors.
*/
readonly mostRecentlyActiveEditors: ReadonlyArray<IEditorIdentifier>;
}

View File

@@ -0,0 +1,396 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor';
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { coalesce } from 'vs/base/common/arrays';
import { LinkedMap, Touch } from 'vs/base/common/map';
import { equals } from 'vs/base/common/objects';
interface ISerializedEditorsList {
entries: ISerializedEditorIdentifier[];
}
interface ISerializedEditorIdentifier {
groupId: GroupIdentifier;
index: number;
}
/**
* A observer of opened editors across all editor groups by most recently used.
* Rules:
* - the last editor in the list is the one most recently activated
* - the first editor in the list is the one that was activated the longest time ago
* - an editor that opens inactive will be placed behind the currently active editor
*
* The observer may start to close editors based on the workbench.editor.limit setting.
*/
export class EditorsObserver extends Disposable {
private static readonly STORAGE_KEY = 'editors.mru';
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
get editors(): IEditorIdentifier[] {
return this.mostRecentEditorsMap.values();
}
constructor(
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group)));
this._register(this.editorGroupsService.onDidEditorPartOptionsChange(e => this.onDidEditorPartOptionsChange(e)));
this.editorGroupsService.whenRestored.then(() => this.loadState());
}
private onGroupAdded(group: IEditorGroup): void {
// Make sure to add any already existing editor
// of the new group into our list in LRU order
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
}
// Make sure that active editor is put as first if group is active
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
// Group Listeners
this.registerGroupListeners(group);
}
private registerGroupListeners(group: IEditorGroup): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidGroupChange(e => {
switch (e.kind) {
// Group gets active: put active editor as most recent
case GroupChangeKind.GROUP_ACTIVE: {
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
break;
}
// Editor gets active: put active editor as most recent
// if group is active, otherwise second most recent
case GroupChangeKind.EDITOR_ACTIVE: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
}
break;
}
// Editor opens: put it as second most recent
//
// Also check for maximum allowed number of editors and
// start to close oldest ones if needed.
case GroupChangeKind.EDITOR_OPEN: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, false /* is not active */);
this.ensureOpenedEditorsLimit({ groupId: group.id, editor: e.editor }, group.id);
}
break;
}
// Editor closes: remove from recently opened
case GroupChangeKind.EDITOR_CLOSE: {
if (e.editor) {
this.removeMostRecentEditor(group, e.editor);
}
break;
}
}
}));
// Make sure to cleanup on dispose
Event.once(group.onWillDispose)(() => dispose(groupDisposables));
}
private onDidEditorPartOptionsChange(event: IEditorPartOptionsChangeEvent): void {
if (!equals(event.newPartOptions.limit, event.oldPartOptions.limit)) {
const activeGroup = this.editorGroupsService.activeGroup;
let exclude: IEditorIdentifier | undefined = undefined;
if (activeGroup.activeEditor) {
exclude = { editor: activeGroup.activeEditor, groupId: activeGroup.id };
}
this.ensureOpenedEditorsLimit(exclude);
}
}
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
const key = this.ensureKey(group, editor);
const mostRecentEditor = this.mostRecentEditorsMap.first;
// Active or first entry: add to end of map
if (isActive || !mostRecentEditor) {
this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined);
}
// Otherwise: insert before most recent
else {
// we have most recent editors. as such we
// put this newly opened editor right before
// the current most recent one because it cannot
// be the most recently active one unless
// it becomes active. but it is still more
// active then any other editor in the list.
this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */);
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
}
// Event
this._onDidChange.fire();
}
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
const key = this.findKey(group, editor);
if (key) {
// Remove from most recent editors
this.mostRecentEditorsMap.delete(key);
// Remove from key map
const map = this.keyMap.get(group.id);
if (map && map.delete(key.editor) && map.size === 0) {
this.keyMap.delete(group.id);
}
// Event
this._onDidChange.fire();
}
}
private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined {
const groupMap = this.keyMap.get(group.id);
if (!groupMap) {
return undefined;
}
return groupMap.get(editor);
}
private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier {
let groupMap = this.keyMap.get(group.id);
if (!groupMap) {
groupMap = new Map();
this.keyMap.set(group.id, groupMap);
}
let key = groupMap.get(editor);
if (!key) {
key = { groupId: group.id, editor };
groupMap.set(editor, key);
}
return key;
}
private async ensureOpenedEditorsLimit(exclude: IEditorIdentifier | undefined, groupId?: GroupIdentifier): Promise<void> {
if (
!this.editorGroupsService.partOptions.limit?.enabled ||
typeof this.editorGroupsService.partOptions.limit.value !== 'number' ||
this.editorGroupsService.partOptions.limit.value <= 0
) {
return; // return early if not enabled or invalid
}
const limit = this.editorGroupsService.partOptions.limit.value;
// In editor group
if (this.editorGroupsService.partOptions.limit?.perEditorGroup) {
// For specific editor groups
if (typeof groupId === 'number') {
const group = this.editorGroupsService.getGroup(groupId);
if (group) {
this.doEnsureOpenedEditorsLimit(limit, group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map(editor => ({ editor, groupId })), exclude);
}
}
// For all editor groups
else {
for (const group of this.editorGroupsService.groups) {
await this.ensureOpenedEditorsLimit(exclude, group.id);
}
}
}
// Across all editor groups
else {
this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude);
}
}
private async doEnsureOpenedEditorsLimit(limit: number, mostRecentEditors: IEditorIdentifier[], exclude?: IEditorIdentifier): Promise<void> {
if (limit >= mostRecentEditors.length) {
return; // only if opened editors exceed setting and is valid and enabled
}
// Extract least recently used editors that can be closed
const leastRecentlyClosableEditors = mostRecentEditors.reverse().filter(({ editor, groupId }) => {
if (editor.isDirty()) {
return false; // not dirty editors
}
if (exclude && editor === exclude.editor && groupId === exclude.groupId) {
return false; // never the editor that should be excluded
}
return true;
});
// Close editors until we reached the limit again
let editorsToCloseCount = mostRecentEditors.length - limit;
const mapGroupToEditorsToClose = new Map<GroupIdentifier, IEditorInput[]>();
for (const { groupId, editor } of leastRecentlyClosableEditors) {
let editorsInGroupToClose = mapGroupToEditorsToClose.get(groupId);
if (!editorsInGroupToClose) {
editorsInGroupToClose = [];
mapGroupToEditorsToClose.set(groupId, editorsInGroupToClose);
}
editorsInGroupToClose.push(editor);
editorsToCloseCount--;
if (editorsToCloseCount === 0) {
break; // limit reached
}
}
for (const [groupId, editors] of mapGroupToEditorsToClose) {
const group = this.editorGroupsService.getGroup(groupId);
if (group) {
await group.closeEditors(editors, { preserveFocus: true });
}
}
}
private saveState(): void {
if (this.mostRecentEditorsMap.isEmpty()) {
this.storageService.remove(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(EditorsObserver.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE);
}
}
private serialize(): ISerializedEditorsList {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const entries = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {
entries: coalesce(entries.map(({ editor, groupId }) => {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
return undefined;
}
// Find serializable editors of group
let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group);
if (!serializableEditorsOfGroup) {
serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => {
const factory = registry.getEditorInputFactory(editor.getTypeId());
return factory?.canSerialize(editor);
});
mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup);
}
// Only store the index of the editor of that group
// which can be undefined if the editor is not serializable
const index = serializableEditorsOfGroup.indexOf(editor);
if (index === -1) {
return undefined;
}
return { groupId, index };
}))
};
}
private loadState(): void {
const serialized = this.storageService.get(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
// Previous state:
if (serialized) {
// Load editors map from persisted state
this.deserialize(JSON.parse(serialized));
}
// No previous state: best we can do is add each editor
// from oldest to most recently used editor group
else {
const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
}
}
}
// Ensure we listen on group changes for those that exist on startup
for (const group of this.editorGroupsService.groups) {
this.registerGroupListeners(group);
}
}
private deserialize(serialized: ISerializedEditorsList): void {
const mapValues: [IEditorIdentifier, IEditorIdentifier][] = [];
for (const { groupId, index } of serialized.entries) {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
continue;
}
// Find editor for entry
const editor = group.getEditorByIndex(index);
if (!editor) {
continue;
}
// Make sure key is registered as well
const editorIdentifier = this.ensureKey(group, editor);
mapValues.push([editorIdentifier, editorIdentifier]);
}
// Fill map with deserialized values
this.mostRecentEditorsMap.fromJSON(mapValues);
}
}

View File

@@ -44,6 +44,7 @@
line-height: 22px;
height: 100%;
vertical-align: top;
max-width: 40vw;
}
.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak {
@@ -94,7 +95,9 @@
height: 100%;
padding: 0 5px 0 5px;
white-space: pre; /* gives some degree of styling */
align-items: center
align-items: center;
text-overflow: ellipsis;
overflow: hidden;
}
.monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover {

View File

@@ -348,7 +348,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
getTitle(): string {
if (this.isViewMergedWithContainer()) {
const paneItemTitle = this.paneItems[0].pane.title;
if (this.options.donotShowContainerTitleWhenMergedWithContainer) {
if (this.options.donotShowContainerTitleWhenMergedWithContainer || this.viewContainer.name === paneItemTitle) {
return this.paneItems[0].pane.title;
}
return paneItemTitle ? `${this.viewContainer.name}: ${paneItemTitle}` : this.viewContainer.name;

View File

@@ -131,6 +131,22 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio
'default': true,
'description': nls.localize('centeredLayoutAutoResize', "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.")
},
'workbench.editor.limit.enabled': {
'type': 'boolean',
'default': false,
'description': nls.localize('limitEditorsEnablement', "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors that are not dirty will close to make space for newly opening editors.")
},
'workbench.editor.limit.value': {
'type': 'number',
'default': 10,
'exclusiveMinimum': 0,
'description': nls.localize('limitEditorsMaximum', "Controls the maximum number of opened editors. Use the `workbench.editor.limit.perEditorGroup` setting to control this limit per editor group or across all groups.")
},
'workbench.editor.limit.perEditorGroup': {
'type': 'boolean',
'default': false,
'description': nls.localize('perEditorGroup', "Controls if the limit of maximum opened editors should apply per editor group or across all editor groups.")
},
'workbench.commandPalette.history': {
'type': 'number',
'description': nls.localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."),