Support for pinned tabs (#97078)

* first cut

* editor title - remove closeAllEditors() method

* editors - do not talk about pinning in settings

* editors - wrap up sticky tabs
This commit is contained in:
Benjamin Pasero 2020-05-06 18:07:57 +02:00 committed by GitHub
parent 965b19a28f
commit b85061a0be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1716 additions and 343 deletions

View file

@ -170,6 +170,12 @@ export interface IEditorOptions {
*/
readonly pinned?: boolean;
/**
* An editor that is sticky moves to the beginning of the editors list within the group and will remain
* there unless explicitly closed. Operations such as "Close All" will not close sticky editors.
*/
readonly sticky?: boolean;
/**
* The index in the document stack where to insert the editor into when opening.
*/

View file

@ -11,7 +11,7 @@ import { IWindowOpenable } from 'vs/platform/windows/common/windows';
import { URI } from 'vs/base/common/uri';
import { ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles';
import { Schemas } from 'vs/base/common/network';
import { IEditorViewState } from 'vs/editor/common/editorCommon';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd';
import { DragMouseEvent } from 'vs/base/browser/mouseEvent';
import { normalizeDriveLetter } from 'vs/base/common/labels';
@ -58,7 +58,7 @@ export interface IDraggedEditor extends IDraggedResource {
content?: string;
encoding?: string;
mode?: string;
viewState?: IEditorViewState;
options?: ITextEditorOptions;
}
export interface ISerializedDraggedEditor {
@ -66,7 +66,7 @@ export interface ISerializedDraggedEditor {
content?: string;
encoding?: string;
mode?: string;
viewState?: IEditorViewState;
options?: ITextEditorOptions;
}
export const CodeDataTransfers = {
@ -90,7 +90,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array<ID
resources.push({
resource: URI.parse(draggedEditor.resource),
content: draggedEditor.content,
viewState: draggedEditor.viewState,
options: draggedEditor.options,
encoding: draggedEditor.encoding,
mode: draggedEditor.mode,
isExternal: false
@ -202,9 +202,9 @@ export class ResourcesDropHandler {
encoding: (untitledOrFileResource as IDraggedEditor).encoding,
mode: (untitledOrFileResource as IDraggedEditor).mode,
options: {
...(untitledOrFileResource as IDraggedEditor).options,
pinned: true,
index: targetIndex,
viewState: (untitledOrFileResource as IDraggedEditor).viewState
index: targetIndex
}
}));
@ -311,7 +311,7 @@ export class ResourcesDropHandler {
}
}
export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: (URI | { resource: URI, isDirectory: boolean })[], event: DragMouseEvent | DragEvent): void {
export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: (URI | { resource: URI, isDirectory: boolean })[], optionsCallback: ((resource: URI) => ITextEditorOptions) | undefined, event: DragMouseEvent | DragEvent): void {
if (resources.length === 0 || !event.dataTransfer) {
return;
}
@ -346,18 +346,30 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources:
const draggedEditors: ISerializedDraggedEditor[] = [];
files.forEach(file => {
let options: ITextEditorOptions | undefined = undefined;
// Try to find editor view state from the visible editors that match given resource
let viewState: IEditorViewState | undefined = undefined;
const textEditorControls = editorService.visibleTextEditorControls;
for (const textEditorControl of textEditorControls) {
if (isCodeEditor(textEditorControl)) {
const model = textEditorControl.getModel();
if (model?.uri?.toString() === file.resource.toString()) {
viewState = withNullAsUndefined(textEditorControl.saveViewState());
break;
}
}
// Use provided callback for editor options
if (typeof optionsCallback === 'function') {
options = optionsCallback(file.resource);
}
// Otherwise try to figure out the view state from opened editors that match
else {
options = {
viewState: (() => {
const textEditorControls = editorService.visibleTextEditorControls;
for (const textEditorControl of textEditorControls) {
if (isCodeEditor(textEditorControl)) {
const model = textEditorControl.getModel();
if (model?.uri?.toString() === file.resource.toString()) {
return withNullAsUndefined(textEditorControl.saveViewState());
}
}
}
return undefined;
})()
};
}
// Try to find encoding and mode from text model
@ -378,7 +390,7 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources:
}
// Add as dragged editor
draggedEditors.push({ resource: file.resource.toString(), content, viewState, encoding, mode });
draggedEditors.push({ resource: file.resource.toString(), content, options, encoding, mode });
});
if (draggedEditors.length) {

View file

@ -42,8 +42,21 @@ function toResource(props: IResourceLabelProps | undefined): URI | undefined {
}
export interface IResourceLabelOptions extends IIconLabelValueOptions {
/**
* A hint to the file kind of the resource.
*/
fileKind?: FileKind;
/**
* File decorations to use for the label.
*/
fileDecorations?: { colors: boolean, badges: boolean };
/**
* Will take the provided label as is and e.g. not override it for untitled files.
*/
forceLabel?: boolean;
}
export interface IFileLabelOptions extends IResourceLabelOptions {
@ -368,7 +381,7 @@ class ResourceLabelWidget extends IconLabel {
const resource = toResource(label);
const isMasterDetail = label?.resource && !URI.isUri(label.resource);
if (!isMasterDetail && resource?.scheme === Schemas.untitled) {
if (!options.forceLabel && !isMasterDetail && resource?.scheme === Schemas.untitled) {
// Untitled labels are very dynamic because they may change
// whenever the content changes (unless a path is associated).
// As such we always ask the actual editor for it's name and
@ -528,7 +541,6 @@ class ResourceLabelWidget extends IconLabel {
);
if (deco) {
this.renderDisposables.add(deco);
if (deco.tooltip) {

View file

@ -7,7 +7,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, TextCompareEditorActiveContext, EditorPinnedContext, EditorGroupEditorsCountContext } from 'vs/workbench/common/editor';
import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, TextCompareEditorActiveContext, EditorPinnedContext, EditorGroupEditorsCountContext, EditorStickyContext } from 'vs/workbench/common/editor';
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
@ -433,6 +433,8 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCo
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '1_close', order: 50 });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: nls.localize('keepOpen', "Keep Open"), precondition: EditorPinnedContext.toNegated() }, group: '3_preview', order: 10, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.PIN_EDITOR_COMMAND_ID, title: nls.localize('pin', "Pin") }, group: '3_preview', order: 20, when: ContextKeyExpr.and(EditorStickyContext.toNegated(), ContextKeyExpr.has('config.workbench.editor.showTabs')) });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.UNPIN_EDITOR_COMMAND_ID, title: nls.localize('unpin', "Unpin") }, group: '3_preview', order: 20, when: ContextKeyExpr.and(EditorStickyContext, ContextKeyExpr.has('config.workbench.editor.showTabs')) });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_UP, title: nls.localize('splitUp', "Split Up") }, group: '5_split', order: 10 });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_DOWN, title: nls.localize('splitDown', "Split Down") }, group: '5_split', order: 20 });
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_LEFT, title: nls.localize('splitLeft', "Split Left") }, group: '5_split', order: 30 });
@ -579,6 +581,8 @@ appendEditorToolItem(
// Editor Commands for Command Palette
const viewCategory = { value: nls.localize('view', "View"), original: 'View' };
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: { value: nls.localize('keepEditor', "Keep Editor"), original: 'Keep Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.PIN_EDITOR_COMMAND_ID, title: { value: nls.localize('pinEditor', "Pin Editor"), original: 'Pin Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.showTabs') });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.UNPIN_EDITOR_COMMAND_ID, title: { value: nls.localize('unpinEditor', "Unpin Editor"), original: 'Unpin Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.showTabs') });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeEditorsInGroup', "Close All Editors in Group"), original: 'Close All Editors in Group' }, category: viewCategory } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: { value: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), original: 'Close Saved Editors in Group' }, category: viewCategory } });
MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeOtherEditors', "Close Other Editors in Group"), original: 'Close Other Editors in Group' }, category: viewCategory } });

View file

@ -500,7 +500,7 @@ export class CloseLeftEditorsInGroupAction extends Action {
async run(context?: IEditorIdentifier): Promise<void> {
const { group, editor } = getTarget(this.editorService, this.editorGroupService, context);
if (group && editor) {
return group.closeEditors({ direction: CloseDirection.LEFT, except: editor });
return group.closeEditors({ direction: CloseDirection.LEFT, except: editor, excludeSticky: true });
}
}
}
@ -514,7 +514,7 @@ function getTarget(editorService: IEditorService, editorGroupService: IEditorGro
return { group: editorGroupService.activeGroup, editor: editorGroupService.activeGroup.activeEditor };
}
export abstract class BaseCloseAllAction extends Action {
abstract class BaseCloseAllAction extends Action {
constructor(
id: string,
@ -554,7 +554,7 @@ export abstract class BaseCloseAllAction extends Action {
// to bring each dirty editor to the front so that the user
// can review if the files should be changed or not.
await Promise.all(this.groupsToClose.map(async groupToClose => {
for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: this.excludeSticky })) {
if (editor.isDirty() && !editor.isSaving() /* ignore editors that are being saved */) {
return groupToClose.openEditor(editor);
}
@ -566,7 +566,7 @@ export abstract class BaseCloseAllAction extends Action {
const dirtyEditorsToConfirm = new Set<string>();
const dirtyEditorsToAutoSave = new Set<IEditorInput>();
for (const editor of this.editorService.editors) {
for (const editor of this.editorService.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: this.excludeSticky }).map(({ editor }) => editor)) {
if (!editor.isDirty() || editor.isSaving()) {
continue; // only interested in dirty editors (unless in the process of saving)
}
@ -601,21 +601,29 @@ export abstract class BaseCloseAllAction extends Action {
confirmation = ConfirmResult.DONT_SAVE;
}
if (confirmation === ConfirmResult.CANCEL) {
return;
// Handle result from asking user
let result: boolean | undefined = undefined;
switch (confirmation) {
case ConfirmResult.CANCEL:
return;
case ConfirmResult.DONT_SAVE:
result = await this.editorService.revertAll({ soft: true, includeUntitled: true, excludeSticky: this.excludeSticky });
break;
case ConfirmResult.SAVE:
result = await this.editorService.saveAll({ reason: saveReason, includeUntitled: true, excludeSticky: this.excludeSticky });
break;
}
if (confirmation === ConfirmResult.DONT_SAVE) {
await this.editorService.revertAll({ soft: true, includeUntitled: true });
} else {
await this.editorService.saveAll({ reason: saveReason, includeUntitled: true });
}
if (!this.workingCopyService.hasDirty) {
// Only continue to close editors if we either have no more dirty
// editors or the result from the save/revert was successful
if (!this.workingCopyService.hasDirty || result) {
return this.doCloseAll();
}
}
protected abstract get excludeSticky(): boolean;
protected abstract doCloseAll(): Promise<void>;
}
@ -636,8 +644,12 @@ export class CloseAllEditorsAction extends BaseCloseAllAction {
super(id, label, Codicon.closeAll.classNames, workingCopyService, fileDialogService, editorGroupService, editorService, filesConfigurationService);
}
protected get excludeSticky(): boolean {
return true;
}
protected async doCloseAll(): Promise<void> {
await Promise.all(this.groupsToClose.map(g => g.closeAllEditors()));
await Promise.all(this.groupsToClose.map(group => group.closeAllEditors({ excludeSticky: true })));
}
}
@ -658,6 +670,10 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction {
super(id, label, undefined, workingCopyService, fileDialogService, editorGroupService, editorService, filesConfigurationService);
}
protected get excludeSticky(): boolean {
return false;
}
protected async doCloseAll(): Promise<void> {
await Promise.all(this.groupsToClose.map(group => group.closeAllEditors()));
@ -680,12 +696,12 @@ export class CloseEditorsInOtherGroupsAction extends Action {
async run(context?: IEditorIdentifier): Promise<void> {
const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup;
await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async g => {
if (groupToSkip && g.id === groupToSkip.id) {
await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async group => {
if (groupToSkip && group.id === groupToSkip.id) {
return;
}
return g.closeAllEditors();
return group.closeAllEditors({ excludeSticky: true });
}));
}
}
@ -707,7 +723,7 @@ export class CloseEditorInAllGroupsAction extends Action {
async run(): Promise<void> {
const activeEditor = this.editorService.activeEditor;
if (activeEditor) {
await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor)));
await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(group => group.closeEditor(activeEditor)));
}
}
}

View file

@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
import * as types from 'vs/base/common/types';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane } from 'vs/workbench/common/editor';
import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, EditorStickyContext, EditorsOrder } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor';
@ -37,6 +37,9 @@ export const LAYOUT_EDITOR_GROUPS_COMMAND_ID = 'layoutEditorGroups';
export const KEEP_EDITOR_COMMAND_ID = 'workbench.action.keepEditor';
export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup';
export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor';
export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor';
export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide';
export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange';
export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange';
@ -258,7 +261,7 @@ function registerDiffEditorCommands(): void {
function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void {
const editorService = accessor.get(IEditorService);
const candidates = [editorService.activeEditorPane, ...editorService.visibleEditorPanes].filter(e => e instanceof TextDiffEditor);
const candidates = [editorService.activeEditorPane, ...editorService.visibleEditorPanes].filter(editor => editor instanceof TextDiffEditor);
if (candidates.length > 0) {
const navigator = (<TextDiffEditor>candidates[0]).getDiffNavigator();
@ -491,7 +494,7 @@ function registerCloseEditorCommands() {
return Promise.all(distinct(contexts.map(c => c.groupId)).map(async groupId => {
const group = editorGroupService.getGroup(groupId);
if (group) {
return group.closeEditors({ savedOnly: true });
return group.closeEditors({ savedOnly: true, excludeSticky: true });
}
}));
}
@ -514,7 +517,7 @@ function registerCloseEditorCommands() {
return Promise.all(distinctGroupIds.map(async groupId => {
const group = editorGroupService.getGroup(groupId);
if (group) {
return group.closeAllEditors();
return group.closeAllEditors({ excludeSticky: true });
}
}));
}
@ -596,7 +599,8 @@ function registerCloseEditorCommands() {
const editors = contexts
.filter(context => context.groupId === groupId)
.map(context => typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) : group.activeEditor);
const editorsToClose = group.editors.filter(e => editors.indexOf(e) === -1);
const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => editors.indexOf(editor) === -1);
if (group.activeEditor) {
group.pinEditor(group.activeEditor);
@ -622,7 +626,7 @@ function registerCloseEditorCommands() {
group.pinEditor(group.activeEditor);
}
return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor });
return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true });
}
}
});
@ -642,6 +646,36 @@ function registerCloseEditorCommands() {
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: PIN_EDITOR_COMMAND_ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(EditorStickyContext.toNegated(), ContextKeyExpr.has('config.workbench.editor.showTabs')),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.Enter),
handler: async (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => {
const editorGroupService = accessor.get(IEditorGroupsService);
const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context));
if (group && editor) {
return group.stickEditor(editor);
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: UNPIN_EDITOR_COMMAND_ID,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(EditorStickyContext, ContextKeyExpr.has('config.workbench.editor.showTabs')),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.Enter),
handler: async (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => {
const editorGroupService = accessor.get(IEditorGroupsService);
const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context));
if (group && editor) {
return group.unstickEditor(editor);
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: SHOW_EDITORS_IN_GROUP,
weight: KeybindingWeight.WorkbenchContrib,

View file

@ -266,7 +266,10 @@ class DropOverlay extends Themable {
}
// Open in target group
const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ pinned: true }));
const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({
pinned: true, // always pin dropped editor
sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state
}));
targetGroup.openEditor(draggedEditor.editor, options);
// Ensure target has focus

View file

@ -17,7 +17,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService';
import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER, EDITOR_GROUP_HEADER_BORDER } from 'vs/workbench/common/theme';
import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions, ICloseAllEditorsOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl';
import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl';
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
@ -441,6 +441,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
options.pinned = this._group.isPinned(activeEditor); // preserve pinned state
options.sticky = this._group.isSticky(activeEditor); // preserve sticky state
options.preserveFocus = true; // handle focus after editor is opened
const activeElement = document.activeElement;
@ -732,6 +733,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
return this._group.count;
}
get stickyCount(): number {
return this._group.stickyCount;
}
get activeEditorPane(): IVisibleEditorPane | undefined {
return this.editorControl ? withNullAsUndefined(this.editorControl.activeEditorPane) : undefined;
}
@ -748,12 +753,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
return this._group.isPinned(editor);
}
isSticky(editorOrIndex: EditorInput | number): boolean {
return this._group.isSticky(editorOrIndex);
}
isActive(editor: EditorInput): boolean {
return this._group.isActive(editor);
}
getEditors(order: EditorsOrder): EditorInput[] {
return this._group.getEditors(order);
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] {
return this._group.getEditors(order, options);
}
getEditorByIndex(index: number): EditorInput | undefined {
@ -794,6 +803,43 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
}
stickEditor(candidate: EditorInput | undefined = this.activeEditor || undefined): void {
this.doStickEditor(candidate, true);
}
unstickEditor(candidate: EditorInput | undefined = this.activeEditor || undefined): void {
this.doStickEditor(candidate, false);
}
private doStickEditor(candidate: EditorInput | undefined, sticky: boolean): void {
if (candidate && this._group.isSticky(candidate) !== sticky) {
const oldIndexOfEditor = this.getIndexOfEditor(candidate);
// Update model
const editor = sticky ? this._group.stick(candidate) : this._group.unstick(candidate);
if (!editor) {
return;
}
// If the index of the editor changed, we need to forward this to
// title control and also make sure to emit this as an event
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
if (sticky) {
this.titleAreaControl.stickEditor(editor);
} else {
this.titleAreaControl.unstickEditor(editor);
}
}
}
invokeWithinContext<T>(fn: (accessor: ServicesAccessor) => T): T {
return this.scopedInstantiationService.invokeFunction(fn);
}
@ -833,11 +879,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Determine options
const openEditorOptions: IEditorOpenOptions = {
index: options ? options.index : undefined,
pinned: !this.accessor.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number'), // unless specified, prefer to pin when opening with index
pinned: options?.sticky || !this.accessor.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number' /* unless specified, prefer to pin when opening with index */) || (typeof options?.index === 'number' && this._group.isSticky(options.index)),
sticky: options?.sticky || (typeof options?.index === 'number' && this._group.isSticky(options.index)),
active: this._group.count === 0 || !options || !options.inactive
};
if (!openEditorOptions.active && !openEditorOptions.pinned && this._group.activeEditor && this._group.isPreview(this._group.activeEditor)) {
if (options?.sticky && typeof options?.index === 'number' && !this._group.isSticky(options.index)) {
// Special case: we are to open an editor sticky but at an index that is not sticky
// In that case we prefer to open the editor at the index but not sticky. This enables
// to drag a sticky editor to an index that is not sticky to unstick it.
openEditorOptions.sticky = false;
}
if (!openEditorOptions.active && !openEditorOptions.pinned && this._group.activeEditor && !this._group.isPinned(this._group.activeEditor)) {
// Special case: we are to open an editor inactive and not pinned, but the current active
// editor is also not pinned, which means it will get replaced with this one. As such,
// the editor can only be active.
@ -1095,8 +1149,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// When moving an editor, try to preserve as much view state as possible by checking
// for the editor to be a text editor and creating the options accordingly if so
const options = getActiveTextEditorOptions(this, editor, EditorOptions.create(moveOptions));
options.pinned = true; // always pin moved editor
const options = getActiveTextEditorOptions(this, editor, EditorOptions.create({
...moveOptions,
pinned: true, // always pin moved editor
sticky: this._group.isSticky(editor) // preserve sticky state
}));
// A move to another group is an open first...
target.openEditor(editor, options);
@ -1377,7 +1434,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
return;
}
const editors = this.getEditorsToClose(args);
const editors = this.doGetEditorsToClose(args);
// Check for dirty and veto
const veto = await this.handleDirtyClosing(editors.slice(0));
@ -1389,31 +1446,31 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
this.doCloseEditors(editors, options);
}
private getEditorsToClose(editors: EditorInput[] | ICloseEditorsFilter): EditorInput[] {
if (Array.isArray(editors)) {
return editors;
private doGetEditorsToClose(args: EditorInput[] | ICloseEditorsFilter): EditorInput[] {
if (Array.isArray(args)) {
return args;
}
const filter = editors;
const filter = args;
const hasDirection = typeof filter.direction === 'number';
let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE); // in MRU order only if direction is not specified
let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE, filter); // in MRU order only if direction is not specified
// Filter: saved or saving only
if (filter.savedOnly) {
editorsToClose = editorsToClose.filter(e => !e.isDirty() || e.isSaving());
editorsToClose = editorsToClose.filter(editor => !editor.isDirty() || editor.isSaving());
}
// Filter: direction (left / right)
else if (hasDirection && filter.except) {
editorsToClose = (filter.direction === CloseDirection.LEFT) ?
editorsToClose.slice(0, this._group.indexOf(filter.except)) :
editorsToClose.slice(this._group.indexOf(filter.except) + 1);
editorsToClose.slice(0, this._group.indexOf(filter.except, editorsToClose)) :
editorsToClose.slice(this._group.indexOf(filter.except, editorsToClose) + 1);
}
// Filter: except
else if (filter.except) {
editorsToClose = editorsToClose.filter(e => !e.matches(filter.except));
editorsToClose = editorsToClose.filter(editor => !editor.matches(filter.except));
}
return editorsToClose;
@ -1444,7 +1501,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
//#region closeAllEditors()
async closeAllEditors(): Promise<void> {
async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> {
if (this.isEmpty) {
// If the group is empty and the request is to close all editors, we still close
@ -1458,30 +1515,34 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
// Check for dirty and veto
const editors = this._group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
const veto = await this.handleDirtyClosing(editors.slice(0));
const veto = await this.handleDirtyClosing(this._group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options));
if (veto) {
return;
}
// Do close
this.doCloseAllEditors();
this.doCloseAllEditors(options);
}
private doCloseAllEditors(): void {
private doCloseAllEditors(options?: ICloseAllEditorsOptions): void {
// Close all inactive editors first
this.editors.forEach(editor => {
const editorsToClose: EditorInput[] = [];
this._group.getEditors(EditorsOrder.SEQUENTIAL, options).forEach(editor => {
if (!this.isActive(editor)) {
this.doCloseInactiveEditor(editor);
}
editorsToClose.push(editor);
});
// Close active editor last
this.doCloseActiveEditor();
// Close active editor last (unless we skip it, e.g. because it is sticky)
if (this.activeEditor && editorsToClose.includes(this.activeEditor)) {
this.doCloseActiveEditor();
}
// Forward to title control
this.titleAreaControl.closeAllEditors();
this.titleAreaControl.closeEditors(editorsToClose);
}
//#endregion

View file

@ -302,6 +302,10 @@ export class EditorsObserver extends Disposable {
return false; // never the editor that should be excluded
}
if (this.editorGroupsService.getGroup(groupId)?.isSticky(editor)) {
return false; // never sticky editors
}
return true;
});

View file

@ -63,8 +63,8 @@
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-right,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off {
padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off:not(.sticky) {
padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left (unless sticky) */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit {
@ -82,6 +82,30 @@
max-width: -moz-fit-content;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky {
/** Sticky tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */
position: sticky;
z-index: 1;
/** Sticky tabs are even and never grow */
flex-basis: 0;
flex-grow: 0;
/** Sticky tabs have a fixed width of 38px */
width: 38px;
min-width: 38px;
max-width: 38px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky {
/** Disable sticky positions for sticky tabs if the available space is too little */
position: static;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-left .action-label {
margin-right: 4px !important;
}
@ -174,6 +198,10 @@
opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/Microsoft/vscode/issues/57819) */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.has-icon-theme) .monaco-icon-label {
text-align: center; /* ensure that sticky tabs without icon have label centered */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-container {
overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */
@ -210,14 +238,15 @@
overflow: visible; /* ...but still show the close button on hover, focus and when dirty */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off:not(.dirty) > .tab-close {
display: none; /* hide the close action bar when we are configured to hide it */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off:not(.dirty) > .tab-close,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.sticky > .tab-close {
display: none; /* hide the close action bar when we are configured to hide it (unless dirty, but always when sticky) */
}
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */
opacity: 1;
}
@ -233,10 +262,10 @@
content: "\ea71"; /* use `circle-filled` icon unicode */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */
opacity: 0.5;
}
@ -257,8 +286,8 @@
padding-right: 10px; /* give a little bit more room if close button is off */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off {
padding-right: 5px; /* we need less room when sizing is shrink */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off:not(.sticky) {
padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky) */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close,

View file

@ -136,10 +136,6 @@ export class NoTabsTitleControl extends TitleControl {
this.ifActiveEditorChanged(() => this.redraw());
}
closeAllEditors(): void {
this.redraw();
}
moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void {
this.ifActiveEditorChanged(() => this.redraw());
}
@ -148,6 +144,14 @@ export class NoTabsTitleControl extends TitleControl {
this.ifEditorIsActive(editor, () => this.redraw());
}
stickEditor(editor: IEditorInput): void {
// Sticky editors are not presented any different with tabs disabled
}
unstickEditor(editor: IEditorInput): void {
// Sticky editors are not presented any different with tabs disabled
}
setActive(isActive: boolean): void {
this.redraw();
}
@ -219,7 +223,6 @@ export class NoTabsTitleControl extends TitleControl {
}
}
private ifEditorIsActive(editor: IEditorInput, fn: () => void): void {
if (this.group.isActive(editor)) {
fn(); // only run if editor is current active

View file

@ -27,7 +27,7 @@ import { getOrSet } from 'vs/base/common/map';
import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme';
import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry';
import { ResourcesDropHandler, fillResourceDataTransfers, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@ -62,6 +62,11 @@ export class TabsTitleControl extends TitleControl {
large: 10
};
private static readonly TAB_SIZES = {
sticky: 38,
fit: 120
};
private titleContainer: HTMLElement | undefined;
private tabsAndActionsContainer: HTMLElement | undefined;
private tabsContainer: HTMLElement | undefined;
@ -387,10 +392,6 @@ export class TabsTitleControl extends TitleControl {
this.handleClosedEditors();
}
closeAllEditors(): void {
this.handleClosedEditors();
}
private handleClosedEditors(): void {
// There are tabs to show
@ -448,7 +449,24 @@ export class TabsTitleControl extends TitleControl {
}
pinEditor(editor: IEditorInput): void {
this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel));
this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel));
}
stickEditor(editor: IEditorInput): void {
this.doHandleStickyEditorChange(editor);
}
unstickEditor(editor: IEditorInput): void {
this.doHandleStickyEditorChange(editor);
}
private doHandleStickyEditorChange(editor: IEditorInput): void {
// Update tab
this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawTab(editor, index, tabContainer, tabLabelWidget, tabLabel));
// A change to the sticky state requires a layout to keep the active editor visible
this.layout(this.dimension);
}
setActive(isGroupActive: boolean): void {
@ -482,7 +500,7 @@ export class TabsTitleControl extends TitleControl {
// As such we need to redraw each label
this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => {
this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel);
this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel);
});
// A change to a label requires a layout to keep the active editor visible
@ -740,10 +758,7 @@ export class TabsTitleControl extends TitleControl {
}
// Apply some datatransfer types to allow for dragging the element outside of the application
const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER });
if (resource) {
this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e);
}
this.doFillResourceDataTransfers(editor, e);
// Fixes https://github.com/Microsoft/vscode/issues/18733
addClass(tab, 'dragged');
@ -998,7 +1013,7 @@ export class TabsTitleControl extends TitleControl {
private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void {
// Label
this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel);
this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel);
// Borders / Outline
const borderRightColor = (this.getColor(TAB_BORDER) || this.getColor(contrastBorder));
@ -1006,10 +1021,12 @@ export class TabsTitleControl extends TitleControl {
tabContainer.style.outlineColor = this.getColor(activeContrastBorder) || '';
// Settings
const isTabSticky = this.group.isSticky(index);
const options = this.accessor.partOptions;
const tabCloseButton = isTabSticky ? 'off' /* treat sticky tabs as tabCloseButton: 'off' */ : options.tabCloseButton;
['off', 'left', 'right'].forEach(option => {
const domAction = options.tabCloseButton === option ? addClass : removeClass;
const domAction = tabCloseButton === option ? addClass : removeClass;
domAction(tabContainer, `close-button-${option}`);
});
@ -1024,13 +1041,37 @@ export class TabsTitleControl extends TitleControl {
removeClass(tabContainer, 'has-icon-theme');
}
// Sticky Tabs need a position to remain at their location
// when scrolling to stay in view (requirement for position: sticky)
if (isTabSticky) {
addClass(tabContainer, 'sticky');
tabContainer.style.left = `${index * TabsTitleControl.TAB_SIZES.sticky}px`;
} else {
removeClass(tabContainer, 'sticky');
tabContainer.style.left = 'auto';
}
// Active / dirty state
this.redrawEditorActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget);
}
private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void {
const name = tabLabel.name;
const description = tabLabel.description || '';
private redrawLabel(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void {
const isTabSticky = this.group.isSticky(index);
// Unless tabs are sticky, show the full label and description
// Sticky tabs will only show an icon if icons are enabled
// or their first character of the name otherwise
let name: string | undefined;
let description: string;
if (isTabSticky) {
const isShowingIcons = this.accessor.partOptions.showIcons && !!this.accessor.partOptions.iconTheme;
name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase();
description = '';
} else {
name = tabLabel.name;
description = tabLabel.description || '';
}
const title = tabLabel.title || '';
if (tabLabel.ariaLabel) {
@ -1044,7 +1085,7 @@ export class TabsTitleControl extends TitleControl {
// Label
tabLabelWidget.setResource(
{ name, description, resource: toResource(editor, { supportSideBySide: SideBySideEditor.BOTH }) },
{ title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) }
{ title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel: isTabSticky }
);
// Tests helper
@ -1156,8 +1197,8 @@ export class TabsTitleControl extends TitleControl {
layout(dimension: Dimension | undefined): void {
this.dimension = dimension;
const activeTab = this.group.activeEditor ? this.getTab(this.group.activeEditor) : undefined;
if (!activeTab || !this.dimension) {
const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined;
if (!activeTabAndIndex || !this.dimension) {
return;
}
@ -1175,20 +1216,66 @@ export class TabsTitleControl extends TitleControl {
}
private doLayout(dimension: Dimension): void {
const activeTab = this.group.activeEditor ? this.getTab(this.group.activeEditor) : undefined;
if (!activeTab) {
return;
const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined;
if (!activeTabAndIndex) {
return; // nothing to do if not editor opened
}
const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar);
// Breadcrumbs
this.doLayoutBreadcrumbs(dimension);
// Tabs
const [activeTab, activeIndex] = activeTabAndIndex;
this.doLayoutTabs(activeTab, activeIndex);
}
private doLayoutBreadcrumbs(dimension: Dimension): void {
if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) {
const tabsScrollbar = assertIsDefined(this.tabsScrollbar);
this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT });
tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`;
}
}
const visibleContainerWidth = tabsContainer.offsetWidth;
const totalContainerWidth = tabsContainer.scrollWidth;
private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void {
const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar);
//
// Synopsis
// - allTabsWidth: sum of all tab widths
// - stickyTabsWidth: sum of all sticky tab widths
// - visibleContainerWidth: size of tab container
// - availableContainerWidth: size of tab container minus size of sticky tabs
//
// [------------------------------ All tabs width ---------------------------------------]
// [------------------- Visible container width -------------------]
// [------ Available container width ------]
// [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ]
// Active Tab Width [-------]
// [------- Active Tab Pos X -------]
// [-- Sticky Tabs Width --]
//
const visibleTabsContainerWidth = tabsContainer.offsetWidth;
const allTabsWidth = tabsContainer.scrollWidth;
let stickyTabsWidth = this.group.stickyCount * TabsTitleControl.TAB_SIZES.sticky;
let activeTabSticky = this.group.isSticky(activeIndex);
let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth;
// Special case: we have sticky tabs but the available space for showing tabs
// is little enough that we need to disable sticky tabs sticky positioning
// so that tabs can be scrolled at naturally.
if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_SIZES.fit) {
addClass(tabsContainer, 'disable-sticky-tabs');
availableTabsContainerWidth = visibleTabsContainerWidth;
stickyTabsWidth = 0;
activeTabSticky = false;
} else {
removeClass(tabsContainer, 'disable-sticky-tabs');
}
let activeTabPosX: number | undefined;
let activeTabWidth: number | undefined;
@ -1200,42 +1287,78 @@ export class TabsTitleControl extends TitleControl {
// Update scrollbar
tabsScrollbar.setScrollDimensions({
width: visibleContainerWidth,
scrollWidth: totalContainerWidth
width: visibleTabsContainerWidth,
scrollWidth: allTabsWidth
});
// Return now if we are blocked to reveal the active tab and clear flag
if (this.blockRevealActiveTab || typeof activeTabPosX !== 'number' || typeof activeTabWidth !== 'number') {
// We also return if the active tab is sticky because this means it is
// always visible anyway.
if (this.blockRevealActiveTab || typeof activeTabPosX !== 'number' || typeof activeTabWidth !== 'number' || activeTabSticky) {
this.blockRevealActiveTab = false;
return;
}
// Reveal the active one
const containerScrollPosX = tabsScrollbar.getScrollPosition().scrollLeft;
const activeTabFits = activeTabWidth <= visibleContainerWidth;
const tabsContainerScrollPosX = tabsScrollbar.getScrollPosition().scrollLeft;
const activeTabFits = activeTabWidth <= availableTabsContainerWidth;
const adjustedActiveTabPosX = activeTabPosX - stickyTabsWidth;
//
// Synopsis
// - adjustedActiveTabPosX: the adjusted tabPosX takes the width of sticky tabs into account
// conceptually the scrolling only begins after sticky tabs so in order to reveal a tab fully
// the actual position needs to be adjusted for sticky tabs.
//
// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
// Note: only try to do this if we actually have enough width to give to show the tab fully!
if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) {
//
// Example: Tab G should be made active and needs to be fully revealed as such.
//
// [-------------------------------- All tabs width -----------------------------------------]
// [-------------------- Visible container width --------------------]
// [----- Available container width -------]
// [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ]
// Active Tab Width [-------]
// [------- Active Tab Pos X -------]
// [-------- Adjusted Tab Pos X -------]
// [-- Sticky Tabs Width --]
//
//
if (activeTabFits && tabsContainerScrollPosX + availableTabsContainerWidth < adjustedActiveTabPosX + activeTabWidth) {
tabsScrollbar.setScrollPosition({
scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
scrollLeft: tabsContainerScrollPosX + ((adjustedActiveTabPosX + activeTabWidth) /* right corner of tab */ - (tabsContainerScrollPosX + availableTabsContainerWidth) /* right corner of view port */)
});
}
// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
//
// Tab is overlflowing to the left or does not fit: Scroll it into view to the left
//
// Example: Tab C should be made active and needs to be fully revealed as such.
//
// [----------------------------- All tabs width ----------------------------------------]
// [------------------ Visible container width ------------------]
// [----- Available container width -------]
// [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ]
// Active Tab Width [-------]
// [------- Active Tab Pos X -------]
// Adjusted Tab Pos X []
// [-- Sticky Tabs Width --]
//
//
else if (tabsContainerScrollPosX > adjustedActiveTabPosX || !activeTabFits) {
tabsScrollbar.setScrollPosition({
scrollLeft: activeTabPosX
scrollLeft: adjustedActiveTabPosX
});
}
}
private getTab(editor: IEditorInput): HTMLElement | undefined {
private getTabAndIndex(editor: IEditorInput): [HTMLElement, number /* index */] | undefined {
const editorIndex = this.group.getIndexOfEditor(editor);
if (editorIndex >= 0) {
const tabsContainer = assertIsDefined(this.tabsContainer);
return tabsContainer.children[editorIndex] as HTMLElement;
return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex];
}
return undefined;
@ -1455,11 +1578,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
// Adjust gradient for focused and unfocused hover background
const makeTabHoverBackgroundRule = (color: Color, colorDrag: Color, hasFocus = false) => `
.monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after {
.monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after {
background: linear-gradient(to left, ${color}, transparent) !important;
}
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after {
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after {
background: linear-gradient(to left, ${colorDrag}, transparent) !important;
}
`;
@ -1482,19 +1605,19 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
if (editorDragAndDropBackground && adjustedTabDragBackground) {
const adjustedColorDrag = editorDragAndDropBackground.flatten(adjustedTabDragBackground);
collector.addRule(`
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged) > .tab-label::after,
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after {
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged):not(.sticky) > .tab-label::after,
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged):not(.sticky) > .tab-label::after {
background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important;
}
`);
}
const makeTabBackgroundRule = (color: Color, colorDrag: Color, focused: boolean, active: boolean) => `
.monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged) > .tab-label::after {
.monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after {
background: linear-gradient(to left, ${color}, transparent);
}
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged) > .tab-label::after {
.monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after {
background: linear-gradient(to left, ${colorDrag}, transparent);
}
`;

View file

@ -173,7 +173,12 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan
// because we are triggering another openEditor() call
// and do not control the initial intent that resulted
// in us now opening as binary.
const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE, pinned: this.group?.isPinned(input) };
const preservingOptions: IEditorOptions = {
activation: EditorActivation.PRESERVE,
pinned: this.group?.isPinned(input),
sticky: this.group?.isSticky(input)
};
if (options) {
options.overwrite(preservingOptions);
} else {
@ -229,7 +234,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan
if (isArray(error)) {
const errors = <Error[]>error;
return errors.some(e => this.isFileBinaryError(e));
return errors.some(error => this.isFileBinaryError(error));
}
return (<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY;

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/titlecontrol';
import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd';
import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@ -12,8 +13,7 @@ import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecut
import * as arrays from 'vs/base/common/arrays';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { dispose, DisposableStore } from 'vs/base/common/lifecycle';
import 'vs/css!./media/titlecontrol';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { getCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { localize } from 'vs/nls';
import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
@ -32,13 +32,14 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl';
import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource, IEditorPartOptions, SideBySideEditor, EditorPinnedContext } from 'vs/workbench/common/editor';
import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource, IEditorPartOptions, SideBySideEditor, EditorPinnedContext, EditorStickyContext } from 'vs/workbench/common/editor';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { IFileService } from 'vs/platform/files/common/files';
import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types';
import { isFirefox } from 'vs/base/browser/browser';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
export interface IToolbarActions {
primary: IAction[];
@ -59,6 +60,7 @@ export abstract class TitleControl extends Themable {
private resourceContext: ResourceContextKey;
private editorPinnedContext: IContextKey<boolean>;
private editorStickyContext: IContextKey<boolean>;
private readonly editorToolBarMenuDisposables = this._register(new DisposableStore());
@ -85,6 +87,7 @@ export abstract class TitleControl extends Themable {
this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));
this.editorPinnedContext = EditorPinnedContext.bindTo(contextKeyService);
this.editorStickyContext = EditorStickyContext.bindTo(contextKeyService);
this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService));
@ -221,6 +224,7 @@ export abstract class TitleControl extends Themable {
// Update contexts
this.resourceContext.set(this.group.activeEditor ? withUndefinedAsNull(toResource(this.group.activeEditor, { supportSideBySide: SideBySideEditor.MASTER })) : null);
this.editorPinnedContext.set(this.group.activeEditor ? this.group.isPinned(this.group.activeEditor) : false);
this.editorStickyContext.set(this.group.activeEditor ? this.group.isSticky(this.group.activeEditor) : false);
// Editor actions require the editor control to be there, so we retrieve it via service
const activeEditorPane = this.group.activeEditorPane;
@ -265,10 +269,8 @@ export abstract class TitleControl extends Themable {
// If tabs are disabled, treat dragging as if an editor tab was dragged
let hasDataTransfer = false;
if (!this.accessor.partOptions.showTabs) {
const resource = this.group.activeEditor ? toResource(this.group.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }) : null;
if (resource) {
this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e);
hasDataTransfer = true;
if (this.group.activeEditor) {
hasDataTransfer = this.doFillResourceDataTransfers(this.group.activeEditor, e);
}
}
@ -294,6 +296,31 @@ export abstract class TitleControl extends Themable {
}));
}
protected doFillResourceDataTransfers(editor: IEditorInput, e: DragEvent): boolean {
const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER });
if (!resource) {
return false;
}
const editorOptions: ITextEditorOptions = {
viewState: (() => {
if (this.group.activeEditor === editor) {
const activeControl = this.group.activeEditorPane?.getControl();
if (isCodeEditor(activeControl)) {
return withNullAsUndefined(activeControl.saveViewState());
}
}
return undefined;
})(),
sticky: this.group.isSticky(editor)
};
this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], () => editorOptions, e);
return true;
}
protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void {
// Update contexts based on editor picked and remember previous to restore
@ -301,6 +328,8 @@ export abstract class TitleControl extends Themable {
this.resourceContext.set(withUndefinedAsNull(toResource(editor, { supportSideBySide: SideBySideEditor.MASTER })));
const currentPinnedContext = !!this.editorPinnedContext.get();
this.editorPinnedContext.set(this.group.isPinned(editor));
const currentStickyContext = !!this.editorStickyContext.get();
this.editorStickyContext.set(this.group.isSticky(editor));
// Find target anchor
let anchor: HTMLElement | { x: number, y: number } = node;
@ -324,6 +353,7 @@ export abstract class TitleControl extends Themable {
// restore previous contexts
this.resourceContext.set(currentResourceContext || null);
this.editorPinnedContext.set(currentPinnedContext);
this.editorStickyContext.set(currentStickyContext);
// restore focus to active group
this.accessor.activeGroup.focus();
@ -350,12 +380,14 @@ export abstract class TitleControl extends Themable {
abstract closeEditors(editors: IEditorInput[]): void;
abstract closeAllEditors(): void;
abstract moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void;
abstract pinEditor(editor: IEditorInput): void;
abstract stickEditor(editor: IEditorInput): void;
abstract unstickEditor(editor: IEditorInput): void;
abstract setActive(isActive: boolean): void;
abstract updateEditorLabel(editor: IEditorInput): void;

View file

@ -108,12 +108,12 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio
},
'workbench.editor.enablePreview': {
'type': 'boolean',
'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing) and show up with an italic font style."),
'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are explicitly set to be kept open (e.g. via double click or editing) and show up with an italic font style."),
'default': true
},
'workbench.editor.enablePreviewFromQuickOpen': {
'type': 'boolean',
'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing)."),
'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors are reused until they are explicitly set to be kept open (e.g. via double click or editing)."),
'default': true
},
'workbench.editor.closeOnFileDelete': {

View file

@ -35,6 +35,7 @@ export const ActiveEditorIsReadonlyContext = new RawContextKey<boolean>('activeE
export const ActiveEditorAvailableEditorIdsContext = new RawContextKey<string>('activeEditorAvailableEditorIds', '');
export const EditorsVisibleContext = new RawContextKey<boolean>('editorIsOpen', false);
export const EditorPinnedContext = new RawContextKey<boolean>('editorPinned', false);
export const EditorStickyContext = new RawContextKey<boolean>('editorSticky', false);
export const EditorGroupActiveEditorDirtyContext = new RawContextKey<boolean>('groupActiveEditorDirty', false);
export const EditorGroupEditorsCountContext = new RawContextKey<number>('groupEditorsCount', 0);
export const NoEditorsVisibleContext = EditorsVisibleContext.toNegated();
@ -1046,6 +1047,12 @@ export class EditorOptions implements IEditorOptions {
*/
pinned: boolean | undefined;
/**
* An editor that is sticky moves to the beginning of the editors list within the group and will remain
* there unless explicitly closed. Operations such as "Close All" will not close sticky editors.
*/
sticky: boolean | undefined;
/**
* The index in the document stack where to insert the editor into when opening.
*/
@ -1111,6 +1118,10 @@ export class EditorOptions implements IEditorOptions {
this.pinned = options.pinned;
}
if (typeof options.sticky === 'boolean') {
this.sticky = options.sticky;
}
if (typeof options.inactive === 'boolean') {
this.inactive = options.inactive;
}
@ -1291,6 +1302,7 @@ export class EditorCommandsContextActionRunner extends ActionRunner {
export interface IEditorCloseEvent extends IEditorIdentifier {
replaced: boolean;
index: number;
sticky: boolean;
}
export type GroupIdentifier = number;

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, CloseDirection, SideBySideEditorInput, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor';
import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { coalesce } from 'vs/base/common/arrays';
@ -29,6 +29,7 @@ export interface EditorIdentifier extends IEditorIdentifier {
export interface IEditorOpenOptions {
pinned?: boolean;
sticky?: boolean;
active?: boolean;
index?: number;
}
@ -43,6 +44,7 @@ export interface ISerializedEditorGroup {
editors: ISerializedEditorInput[];
mru: number[];
preview?: number;
sticky?: number;
}
export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup {
@ -91,6 +93,7 @@ export class EditorGroup extends Disposable {
private preview: EditorInput | null = null; // editor in preview state
private active: EditorInput | null = null; // editor in active state
private sticky: number = -1; // index of first editor in sticky state
private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined;
private focusRecentEditorAfterClose: boolean | undefined;
@ -113,10 +116,10 @@ export class EditorGroup extends Disposable {
}
private registerListeners(): void {
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
this._register(this.configurationService.onDidChangeConfiguration(() => this.onConfigurationUpdated()));
}
private onConfigurationUpdated(event?: IConfigurationChangeEvent): void {
private onConfigurationUpdated(): void {
this.editorOpenPositioning = this.configurationService.getValue('workbench.editor.openPositioning');
this.focusRecentEditorAfterClose = this.configurationService.getValue('workbench.editor.focusRecentEditorAfterClose');
}
@ -125,8 +128,25 @@ export class EditorGroup extends Disposable {
return this.editors.length;
}
getEditors(order: EditorsOrder): EditorInput[] {
return order === EditorsOrder.MOST_RECENTLY_ACTIVE ? this.mru.slice(0) : this.editors.slice(0);
get stickyCount(): number {
return this.sticky + 1;
}
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] {
const editors = order === EditorsOrder.MOST_RECENTLY_ACTIVE ? this.mru.slice(0) : this.editors.slice(0);
if (options?.excludeSticky) {
// MRU: need to check for index on each
if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) {
return editors.filter(editor => !this.isSticky(editor));
}
// Sequential: simply start after sticky index
return editors.slice(this.sticky + 1);
}
return editors;
}
getEditorByIndex(index: number): EditorInput | undefined {
@ -145,18 +165,15 @@ export class EditorGroup extends Disposable {
return this.preview;
}
isPreview(editor: EditorInput): boolean {
return this.matches(this.preview, editor);
}
openEditor(candidate: EditorInput, options?: IEditorOpenOptions): EditorInput {
const makePinned = options?.pinned;
const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index));
const makePinned = options?.pinned || options?.sticky;
const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor));
const existingEditor = this.findEditor(candidate);
const existingEditorAndIndex = this.findEditor(candidate);
// New editor
if (!existingEditor) {
if (!existingEditorAndIndex) {
const newEditor = candidate;
const indexOfActive = this.indexOf(this.active);
@ -169,6 +186,12 @@ export class EditorGroup extends Disposable {
// Insert to the BEGINNING
else if (this.editorOpenPositioning === EditorOpenPositioning.FIRST) {
targetIndex = 0;
// Always make sure targetIndex is after sticky editors
// unless we are explicitly told to make the editor sticky
if (!makeSticky && this.isSticky(targetIndex)) {
targetIndex = this.sticky + 1;
}
}
// Insert to the END
@ -176,18 +199,38 @@ export class EditorGroup extends Disposable {
targetIndex = this.editors.length;
}
// Insert to the LEFT of active editor
else if (this.editorOpenPositioning === EditorOpenPositioning.LEFT) {
if (indexOfActive === 0 || !this.editors.length) {
targetIndex = 0; // to the left becoming first editor in list
} else {
targetIndex = indexOfActive; // to the left of active editor
// Insert to LEFT or RIGHT of active editor
else {
// Insert to the LEFT of active editor
if (this.editorOpenPositioning === EditorOpenPositioning.LEFT) {
if (indexOfActive === 0 || !this.editors.length) {
targetIndex = 0; // to the left becoming first editor in list
} else {
targetIndex = indexOfActive; // to the left of active editor
}
}
// Insert to the RIGHT of active editor
else {
targetIndex = indexOfActive + 1;
}
// Always make sure targetIndex is after sticky editors
// unless we are explicitly told to make the editor sticky
if (!makeSticky && this.isSticky(targetIndex)) {
targetIndex = this.sticky + 1;
}
}
// Insert to the RIGHT of active editor
else {
targetIndex = indexOfActive + 1;
// If the editor becomes sticky, increment the sticky index and adjust
// the targetIndex to be at the end of sticky editors unless already.
if (makeSticky) {
this.sticky++;
if (!this.isSticky(targetIndex)) {
targetIndex = this.sticky;
}
}
// Insert into our list of editors if pinned or we have no preview editor
@ -227,6 +270,7 @@ export class EditorGroup extends Disposable {
// Existing editor
else {
const [existingEditor] = existingEditorAndIndex;
// Pin it
if (makePinned) {
@ -243,6 +287,12 @@ export class EditorGroup extends Disposable {
this.moveEditor(existingEditor, options.index);
}
// Stick it (intentionally after the moveEditor call in case
// the editor was already moved into the sticky range)
if (makeSticky) {
this.doStick(existingEditor, this.indexOf(existingEditor));
}
return existingEditor;
}
}
@ -251,8 +301,7 @@ export class EditorGroup extends Disposable {
const listeners = new DisposableStore();
// Re-emit disposal of editor input as our own event
const onceDispose = Event.once(editor.onDispose);
listeners.add(onceDispose(() => {
listeners.add(Event.once(editor.onDispose)(() => {
if (this.indexOf(editor) >= 0) {
this._onDidDisposeEditor.fire(editor);
}
@ -308,6 +357,7 @@ export class EditorGroup extends Disposable {
}
const editor = this.editors[index];
const sticky = this.isSticky(index);
// Active Editor closed
if (openNext && this.matches(this.active, editor)) {
@ -343,45 +393,18 @@ export class EditorGroup extends Disposable {
this.splice(index, true);
// Event
return { editor, replaced, index, groupId: this.id };
}
closeEditors(except: EditorInput, direction?: CloseDirection): void {
const index = this.indexOf(except);
if (index === -1) {
return; // not found
}
// Close to the left
if (direction === CloseDirection.LEFT) {
for (let i = index - 1; i >= 0; i--) {
this.closeEditor(this.editors[i]);
}
}
// Close to the right
else if (direction === CloseDirection.RIGHT) {
for (let i = this.editors.length - 1; i > index; i--) {
this.closeEditor(this.editors[i]);
}
}
// Both directions
else {
this.mru.filter(e => !this.matches(e, except)).forEach(e => this.closeEditor(e));
}
}
closeAllEditors(): void {
// Optimize: close all non active editors first to produce less upstream work
this.mru.filter(e => !this.matches(e, this.active)).forEach(e => this.closeEditor(e));
if (this.active) {
this.closeEditor(this.active);
}
return { editor, replaced, sticky, index, groupId: this.id };
}
moveEditor(candidate: EditorInput, toIndex: number): EditorInput | undefined {
// Ensure toIndex is in bounds of our model
if (toIndex >= this.editors.length) {
toIndex = this.editors.length - 1;
} else if (toIndex < 0) {
toIndex = 0;
}
const index = this.indexOf(candidate);
if (index < 0 || toIndex === index) {
return;
@ -389,6 +412,16 @@ export class EditorGroup extends Disposable {
const editor = this.editors[index];
// Adjust sticky index: editor moved out of sticky state into unsticky state
if (this.isSticky(index) && toIndex > this.sticky) {
this.sticky--;
}
// ...or editor moved into sticky state from unsticky state
else if (!this.isSticky(index) && toIndex <= this.sticky) {
this.sticky++;
}
// Move
this.editors.splice(index, 1);
this.editors.splice(toIndex, 0, editor);
@ -400,11 +433,13 @@ export class EditorGroup extends Disposable {
}
setActive(candidate: EditorInput): EditorInput | undefined {
const editor = this.findEditor(candidate);
if (!editor) {
const res = this.findEditor(candidate);
if (!res) {
return; // not found
}
const [editor] = res;
this.doSetActive(editor);
return editor;
@ -427,18 +462,20 @@ export class EditorGroup extends Disposable {
}
pin(candidate: EditorInput): EditorInput | undefined {
const editor = this.findEditor(candidate);
if (!editor) {
const res = this.findEditor(candidate);
if (!res) {
return; // not found
}
const [editor] = res;
this.doPin(editor);
return editor;
}
private doPin(editor: EditorInput): void {
if (!this.isPreview(editor)) {
if (this.isPinned(editor)) {
return; // can only pin a preview editor
}
@ -450,11 +487,13 @@ export class EditorGroup extends Disposable {
}
unpin(candidate: EditorInput): EditorInput | undefined {
const editor = this.findEditor(candidate);
if (!editor) {
const res = this.findEditor(candidate);
if (!res) {
return; // not found
}
const [editor] = res;
this.doUnpin(editor);
return editor;
@ -478,33 +517,97 @@ export class EditorGroup extends Disposable {
}
}
isPinned(editor: EditorInput): boolean;
isPinned(index: number): boolean;
isPinned(arg1: EditorInput | number): boolean {
if (!this.preview) {
return true; // no preview editor
}
isPinned(editorOrIndex: EditorInput | number): boolean {
let editor: EditorInput;
let index: number;
if (typeof arg1 === 'number') {
editor = this.editors[arg1];
index = arg1;
if (typeof editorOrIndex === 'number') {
editor = this.editors[editorOrIndex];
} else {
editor = arg1;
index = this.indexOf(editor);
}
if (index === -1 || !editor) {
return false; // editor not found
editor = editorOrIndex;
}
return !this.matches(this.preview, editor);
}
stick(candidate: EditorInput): EditorInput | undefined {
const res = this.findEditor(candidate);
if (!res) {
return; // not found
}
const [editor, index] = res;
this.doStick(editor, index);
return editor;
}
private doStick(editor: EditorInput, index: number): void {
if (this.isSticky(index)) {
return; // can only stick a non-sticky editor
}
// Pin editor
this.pin(editor);
// Move editor to be the last sticky editor
this.moveEditor(editor, this.sticky + 1);
// Adjust sticky index
this.sticky++;
}
unstick(candidate: EditorInput): EditorInput | undefined {
const res = this.findEditor(candidate);
if (!res) {
return; // not found
}
const [editor, index] = res;
this.doUnstick(editor, index);
return editor;
}
private doUnstick(editor: EditorInput, index: number): void {
if (!this.isSticky(index)) {
return; // can only unstick a sticky editor
}
// Move editor to be the first non-sticky editor
this.moveEditor(editor, this.sticky);
// Adjust sticky index
this.sticky--;
}
isSticky(candidateOrIndex: EditorInput | number): boolean {
if (this.sticky < 0) {
return false; // no sticky editor
}
let index: number;
if (typeof candidateOrIndex === 'number') {
index = candidateOrIndex;
} else {
index = this.indexOf(candidateOrIndex);
}
if (index < 0) {
return false;
}
return index <= this.sticky;
}
private splice(index: number, del: boolean, editor?: EditorInput): void {
const editorToDeleteOrReplace = this.editors[index];
// Perform on sticky index
if (del && this.isSticky(index)) {
this.sticky--;
}
// Perform on editors array
if (editor) {
this.editors.splice(index, del ? 1 : 0, editor);
@ -512,35 +615,38 @@ export class EditorGroup extends Disposable {
this.editors.splice(index, del ? 1 : 0);
}
// Add
if (!del && editor) {
if (this.mru.length === 0) {
// the list of most recent editors is empty
// so this editor can only be the most recent
this.mru.push(editor);
} else {
// we have most recent editors. as such we
// put this newly opened editor right after
// 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.mru.splice(1, 0, editor);
}
}
// Remove / Replace
else {
const indexInMRU = this.indexOf(editorToDeleteOrReplace, this.mru);
// Remove
if (del && !editor) {
this.mru.splice(indexInMRU, 1); // remove from MRU
// Perform on MRU
{
// Add
if (!del && editor) {
if (this.mru.length === 0) {
// the list of most recent editors is empty
// so this editor can only be the most recent
this.mru.push(editor);
} else {
// we have most recent editors. as such we
// put this newly opened editor right after
// 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.mru.splice(1, 0, editor);
}
}
// Replace
else if (del && editor) {
this.mru.splice(indexInMRU, 1, editor); // replace MRU at location
// Remove / Replace
else {
const indexInMRU = this.indexOf(editorToDeleteOrReplace, this.mru);
// Remove
if (del && !editor) {
this.mru.splice(indexInMRU, 1); // remove from MRU
}
// Replace
else if (del && editor) {
this.mru.splice(indexInMRU, 1, editor); // replace MRU at location
}
}
}
}
@ -559,13 +665,13 @@ export class EditorGroup extends Disposable {
return -1;
}
private findEditor(candidate: EditorInput | null): EditorInput | undefined {
private findEditor(candidate: EditorInput | null): [EditorInput, number /* index */] | undefined {
const index = this.indexOf(candidate, this.editors);
if (index === -1) {
return undefined;
}
return this.editors[index];
return [this.editors[index], index];
}
contains(candidate: EditorInput, searchInSideBySideEditors?: boolean): boolean {
@ -598,7 +704,7 @@ export class EditorGroup extends Disposable {
group.mru = this.mru.slice(0);
group.preview = this.preview;
group.active = this.active;
group.editorOpenPositioning = this.editorOpenPositioning;
group.sticky = this.sticky;
return group;
}
@ -608,32 +714,52 @@ export class EditorGroup extends Disposable {
// Serialize all editor inputs so that we can store them.
// Editors that cannot be serialized need to be ignored
// from mru, active and preview if any.
// from mru, active, preview and sticky if any.
let serializableEditors: EditorInput[] = [];
let serializedEditors: ISerializedEditorInput[] = [];
let serializablePreviewIndex: number | undefined;
this.editors.forEach(e => {
const factory = registry.getEditorInputFactory(e.getTypeId());
if (factory) {
const value = factory.serialize(e);
if (typeof value === 'string') {
serializedEditors.push({ id: e.getTypeId(), value });
serializableEditors.push(e);
let serializableSticky = this.sticky;
if (this.preview === e) {
for (let i = 0; i < this.editors.length; i++) {
const editor = this.editors[i];
let canSerializeEditor = false;
const factory = registry.getEditorInputFactory(editor.getTypeId());
if (factory) {
const value = factory.serialize(editor);
// Editor can be serialized
if (typeof value === 'string') {
canSerializeEditor = true;
serializedEditors.push({ id: editor.getTypeId(), value });
serializableEditors.push(editor);
if (this.preview === editor) {
serializablePreviewIndex = serializableEditors.length - 1;
}
}
}
});
const serializableMru = this.mru.map(e => this.indexOf(e, serializableEditors)).filter(i => i >= 0);
// Editor cannot be serialized
else {
canSerializeEditor = false;
}
}
// Adjust index of sticky editors if the editor cannot be serialized and is pinned
if (!canSerializeEditor && this.isSticky(i)) {
serializableSticky--;
}
}
const serializableMru = this.mru.map(editor => this.indexOf(editor, serializableEditors)).filter(i => i >= 0);
return {
id: this.id,
editors: serializedEditors,
mru: serializableMru,
preview: serializablePreviewIndex,
sticky: serializableSticky >= 0 ? serializableSticky : undefined
};
}
@ -648,18 +774,22 @@ export class EditorGroup extends Disposable {
this._id = EditorGroup.IDS++; // backwards compatibility
}
this.editors = coalesce(data.editors.map(e => {
this.editors = coalesce(data.editors.map((e, index) => {
let editor: EditorInput | undefined = undefined;
const factory = registry.getEditorInputFactory(e.id);
if (factory) {
const editor = factory.deserialize(this.instantiationService, e.value);
editor = factory.deserialize(this.instantiationService, e.value);
if (editor) {
this.registerEditorListeners(editor);
}
return editor;
}
return null;
if (!editor && typeof data.sticky === 'number' && index <= data.sticky) {
data.sticky--; // if editor cannot be deserialized but was sticky, we need to decrease sticky index
}
return editor;
}));
this.mru = coalesce(data.mru.map(i => this.editors[i]));
@ -670,6 +800,10 @@ export class EditorGroup extends Disposable {
this.preview = this.editors[data.preview];
}
if (typeof data.sticky === 'number') {
this.sticky = data.sticky;
}
return this._id;
}
}

View file

@ -890,7 +890,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
if (items && items.length && originalEvent.dataTransfer) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent);
// The only custom data transfer we set from the explorer is a file transfer
// to be able to DND between multiple code file explorers across windows
@ -965,7 +965,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const droppedResources = extractResources(originalEvent, true);
// Check for dropped external files to be folders
const result = await this.fileService.resolveAll(droppedResources);
const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource })));
// Pass focus to window
this.hostService.focus();

View file

@ -660,7 +660,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGro
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View file

@ -849,7 +849,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop<TreeElement> {
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View file

@ -378,7 +378,7 @@ export class SearchDND implements ITreeDragAndDrop<RenderableMatch> {
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View file

@ -258,6 +258,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
const optionOverrides = {
preserveFocus: true,
pinned: group.isPinned(editor),
sticky: group.isSticky(editor),
index: group.getIndexOfEditor(editor),
inactive: !group.isActive(editor)
};
@ -428,7 +429,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor);
}
getEditors(order: EditorsOrder): ReadonlyArray<IEditorIdentifier> {
getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier> {
if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) {
return this.editorsObserver.editors;
}
@ -436,7 +439,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
const editors: IEditorIdentifier[] = [];
this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => {
editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL).map(editor => ({ editor, groupId: group.id })));
editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL, options).map(editor => ({ editor, groupId: group.id })));
});
return editors;
@ -1038,7 +1041,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return this.save(this.getAllDirtyEditors(options), options);
}
async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<void> {
async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> {
// Convert to array
if (!Array.isArray(editors)) {
@ -1056,9 +1059,11 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return editor.revert(groupId, options);
}));
return !uniqueEditors.some(({ editor }) => editor.isDirty());
}
async revertAll(options?: IRevertAllEditorsOptions): Promise<void> {
async revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> {
return this.revert(this.getAllDirtyEditors(options), options);
}
@ -1067,9 +1072,19 @@ export class EditorService extends Disposable implements EditorServiceImpl {
for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
if (editor.isDirty() && (!editor.isUntitled() || !!options?.includeUntitled)) {
editors.push({ groupId: group.id, editor });
if (!editor.isDirty()) {
continue;
}
if (!options?.includeUntitled && editor.isUntitled()) {
continue;
}
if (options?.excludeSticky && group.isSticky(editor)) {
continue;
}
editors.push({ groupId: group.id, editor });
}
}
@ -1210,7 +1225,15 @@ export class DelegatingEditorService implements IEditorService {
get editors(): ReadonlyArray<IEditorInput> { return this.editorService.editors; }
get count(): number { return this.editorService.count; }
getEditors(order: EditorsOrder): ReadonlyArray<IEditorIdentifier> { return this.editorService.getEditors(order); }
getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier> {
if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) {
return this.editorService.getEditors(order);
}
return this.editorService.getEditors(order, options);
}
openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
@ -1237,8 +1260,8 @@ export class DelegatingEditorService implements IEditorService {
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> { return this.editorService.save(editors, options); }
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> { return this.editorService.saveAll(options); }
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<void> { return this.editorService.revert(editors, options); }
revertAll(options?: IRevertAllEditorsOptions): Promise<void> { return this.editorService.revertAll(options); }
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> { return this.editorService.revert(editors, options); }
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> { return this.editorService.revertAll(options); }
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable {
throw new Error('Method not implemented.');

View file

@ -77,10 +77,6 @@ export interface EditorGroupLayout {
groups: GroupLayoutArgument[];
}
export interface ICloseEditorOptions {
preserveFocus?: boolean;
}
export interface IMoveEditorOptions {
index?: number;
inactive?: boolean;
@ -103,12 +99,21 @@ export interface IMergeGroupOptions {
index?: number;
}
export interface ICloseEditorOptions {
preserveFocus?: boolean;
}
export type ICloseEditorsFilter = {
except?: IEditorInput,
direction?: CloseDirection,
savedOnly?: boolean
savedOnly?: boolean,
excludeSticky?: boolean
};
export interface ICloseAllEditorsOptions {
excludeSticky?: boolean;
}
export interface IEditorReplacement {
editor: IEditorInput;
replacement: IEditorInput;
@ -419,10 +424,15 @@ export interface IEditorGroup {
readonly previewEditor: IEditorInput | null;
/**
* The number of opend editors in this group.
* The number of opened editors in this group.
*/
readonly count: number;
/**
* The number of sticky editors in this group.
*/
readonly stickyCount: number;
/**
* All opened editors in the group in sequential order of their appearance.
*/
@ -432,8 +442,9 @@ export interface IEditorGroup {
* Get all editors that are currently opened in the group.
*
* @param order the order of the editors to use
* @param options options to select only specific editors as instructed
*/
getEditors(order: EditorsOrder): ReadonlyArray<IEditorInput>;
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorInput>;
/**
* Returns the editor at a specific index of the group.
@ -475,6 +486,11 @@ export interface IEditorGroup {
*/
isPinned(editor: IEditorInput): boolean;
/**
* Find out if the provided editor or index of editor is sticky in the group.
*/
isSticky(editorOrIndex: IEditorInput | number): boolean;
/**
* Find out if the provided editor is active in the group.
*/
@ -517,7 +533,7 @@ export interface IEditorGroup {
*
* @returns a promise when all editors are closed.
*/
closeAllEditors(): Promise<void>;
closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void>;
/**
* Replaces editors in this group with the provided replacement.
@ -538,6 +554,24 @@ export interface IEditorGroup {
*/
pinEditor(editor?: IEditorInput): void;
/**
* Set an editor to be sticky. A sticky editor is showing in the beginning
* of the tab stripe and will not be impacted by close operations.
*
* @param editor the editor to make sticky, or the currently active editor
* if unspecified.
*/
stickEditor(editor?: IEditorInput): void;
/**
* Set an editor to be non-sticky and thus moves back to a location after
* sticky editors and can be closed normally.
*
* @param editor the editor to make unsticky, or the currently active editor
* if unspecified.
*/
unstickEditor(editor?: IEditorInput): void;
/**
* Move keyboard focus into the group.
*/

View file

@ -62,6 +62,11 @@ export interface IBaseSaveRevertAllEditorOptions {
* Whether to include untitled editors as well.
*/
readonly includeUntitled?: boolean;
/**
* Whether to exclude sticky editors.
*/
readonly excludeSticky?: boolean;
}
export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRevertAllEditorOptions { }
@ -166,7 +171,8 @@ export interface IEditorService {
*
* @param order the order of the editors to use
*/
getEditors(order: EditorsOrder): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray<IEditorIdentifier>;
getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier>;
/**
* Open an editor in an editor group.
@ -262,11 +268,15 @@ export interface IEditorService {
/**
* Reverts the provided list of editors.
*
* @returns `true` if all editors reverted and `false` otherwise.
*/
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<void>;
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean>;
/**
* Reverts all editors.
*
* @returns `true` if all editors reverted and `false` otherwise.
*/
revertAll(options?: IRevertAllEditorsOptions): Promise<void>;
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean>;
}

View file

@ -461,7 +461,11 @@ suite('EditorGroupsService', () => {
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
await group.openEditors([
{ editor: input, options: { pinned: true } },
{ editor: inputInactive }
]);
assert.equal(group.count, 2);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
@ -480,7 +484,12 @@ suite('EditorGroupsService', () => {
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
await group.openEditors([
{ editor: input1, options: { pinned: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
@ -492,6 +501,42 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('closeEditors (except one, sticky editor)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([
{ editor: input1, options: { pinned: true, sticky: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ except: input2, excludeSticky: true });
assert.equal(group.count, 2);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
await group.closeEditors({ except: input2 });
assert.equal(group.count, 1);
assert.equal(group.stickyCount, 0);
assert.equal(group.getEditorByIndex(0), input2);
part.dispose();
});
test('closeEditors (saved only)', async () => {
const [part] = createPart();
const group = part.activeGroup;
@ -501,7 +546,12 @@ suite('EditorGroupsService', () => {
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
await group.openEditors([
{ editor: input1, options: { pinned: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
@ -512,6 +562,38 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('closeEditors (saved only, sticky editor)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([
{ editor: input1, options: { pinned: true, sticky: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ savedOnly: true, excludeSticky: true });
assert.equal(group.count, 1);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
await group.closeEditors({ savedOnly: true });
assert.equal(group.count, 0);
part.dispose();
});
test('closeEditors (direction: right)', async () => {
const [part] = createPart();
const group = part.activeGroup;
@ -521,7 +603,12 @@ suite('EditorGroupsService', () => {
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
await group.openEditors([
{ editor: input1, options: { pinned: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
@ -534,6 +621,40 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('closeEditors (direction: right, sticky editor)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([
{ editor: input1, options: { pinned: true, sticky: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2, excludeSticky: true });
assert.equal(group.count, 2);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 });
assert.equal(group.count, 2);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
part.dispose();
});
test('closeEditors (direction: left)', async () => {
const [part] = createPart();
const group = part.activeGroup;
@ -543,7 +664,12 @@ suite('EditorGroupsService', () => {
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
await group.openEditors([
{ editor: input1, options: { pinned: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
@ -556,6 +682,41 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('closeEditors (direction: left, sticky editor)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await group.openEditors([
{ editor: input1, options: { pinned: true, sticky: true } },
{ editor: input2, options: { pinned: true } },
{ editor: input3 }
]);
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ direction: CloseDirection.LEFT, except: input2, excludeSticky: true });
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ direction: CloseDirection.LEFT, except: input2 });
assert.equal(group.count, 2);
assert.equal(group.getEditorByIndex(0), input2);
assert.equal(group.getEditorByIndex(1), input3);
part.dispose();
});
test('closeAllEditors', async () => {
const [part] = createPart();
const group = part.activeGroup;
@ -564,7 +725,11 @@ suite('EditorGroupsService', () => {
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
await group.openEditors([
{ editor: input, options: { pinned: true } },
{ editor: inputInactive }
]);
assert.equal(group.count, 2);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
@ -574,6 +739,35 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('closeAllEditors (sticky editor)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
await group.openEditors([
{ editor: input, options: { pinned: true, sticky: true } },
{ editor: inputInactive }
]);
assert.equal(group.count, 2);
assert.equal(group.stickyCount, 1);
await group.closeAllEditors({ excludeSticky: true });
assert.equal(group.count, 1);
assert.equal(group.stickyCount, 1);
assert.equal(group.getEditorByIndex(0), input);
await group.closeAllEditors();
assert.equal(group.isEmpty, true);
part.dispose();
});
test('moveEditor (same group)', async () => {
const [part] = createPart();
const group = part.activeGroup;
@ -724,4 +918,107 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('sticky editors', async () => {
const [part] = createPart();
const group = part.activeGroup;
await part.whenRestored;
assert.equal(group.stickyCount, 0);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 0);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 0);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 0);
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
await group.openEditor(input, EditorOptions.create({ pinned: true }));
await group.openEditor(inputInactive, EditorOptions.create({ inactive: true }));
assert.equal(group.stickyCount, 0);
assert.equal(group.isSticky(input), false);
assert.equal(group.isSticky(inputInactive), false);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 2);
group.stickEditor(input);
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input), true);
assert.equal(group.isSticky(inputInactive), false);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 1);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 1);
group.unstickEditor(input);
assert.equal(group.stickyCount, 0);
assert.equal(group.isSticky(input), false);
assert.equal(group.isSticky(inputInactive), false);
assert.equal(group.getIndexOfEditor(input), 0);
assert.equal(group.getIndexOfEditor(inputInactive), 1);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 2);
let editorMoveCounter = 0;
const editorGroupChangeListener = group.onDidGroupChange(e => {
if (e.kind === GroupChangeKind.EDITOR_MOVE) {
assert.ok(e.editor);
editorMoveCounter++;
}
});
group.stickEditor(inputInactive);
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input), false);
assert.equal(group.isSticky(inputInactive), true);
assert.equal(group.getIndexOfEditor(input), 1);
assert.equal(group.getIndexOfEditor(inputInactive), 0);
assert.equal(editorMoveCounter, 1);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 1);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 1);
const inputSticky = new TestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID);
await group.openEditor(inputSticky, EditorOptions.create({ sticky: true }));
assert.equal(group.stickyCount, 2);
assert.equal(group.isSticky(input), false);
assert.equal(group.isSticky(inputInactive), true);
assert.equal(group.isSticky(inputSticky), true);
assert.equal(group.getIndexOfEditor(inputInactive), 0);
assert.equal(group.getIndexOfEditor(inputSticky), 1);
assert.equal(group.getIndexOfEditor(input), 2);
await group.openEditor(input, EditorOptions.create({ sticky: true }));
assert.equal(group.stickyCount, 3);
assert.equal(group.isSticky(input), true);
assert.equal(group.isSticky(inputInactive), true);
assert.equal(group.isSticky(inputSticky), true);
assert.equal(group.getIndexOfEditor(inputInactive), 0);
assert.equal(group.getIndexOfEditor(inputSticky), 1);
assert.equal(group.getIndexOfEditor(input), 2);
editorGroupChangeListener.dispose();
part.dispose();
});
});

View file

@ -146,6 +146,22 @@ suite('EditorService', () => {
assert.equal(activeEditorChangeEventCounter, 4);
assert.equal(visibleEditorChangeEventCounter, 4);
const stickyInput = new TestFileEditorInput(URI.parse('my://resource3-basics'), TEST_EDITOR_INPUT_ID);
await service.openEditor(stickyInput, { sticky: true });
assert.equal(3, service.count);
const allSequentialEditors = service.getEditors(EditorsOrder.SEQUENTIAL);
assert.equal(allSequentialEditors.length, 3);
assert.equal(stickyInput, allSequentialEditors[0].editor);
assert.equal(input, allSequentialEditors[1].editor);
assert.equal(otherInput, allSequentialEditors[2].editor);
const sequentialEditorsExcludingSticky = service.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true });
assert.equal(sequentialEditorsExcludingSticky.length, 2);
assert.equal(input, sequentialEditorsExcludingSticky[0].editor);
assert.equal(otherInput, sequentialEditorsExcludingSticky[1].editor);
activeEditorChangeListener.dispose();
visibleEditorChangeListener.dispose();
didCloseEditorListener.dispose();
@ -795,6 +811,10 @@ suite('EditorService', () => {
input1.gotSavedAs = false;
input1.gotReverted = false;
input1.dirty = true;
input2.dirty = true;
sameInput1.dirty = true;
await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true });
assert.equal(input1.gotSavedAs, true);
@ -802,14 +822,24 @@ suite('EditorService', () => {
input1.gotSavedAs = false;
input1.gotReverted = false;
await service.revertAll();
input1.dirty = true;
input2.dirty = true;
sameInput1.dirty = true;
const revertRes = await service.revertAll();
assert.equal(revertRes, true);
assert.equal(input1.gotReverted, true);
input1.gotSaved = false;
input1.gotSavedAs = false;
input1.gotReverted = false;
await service.saveAll();
input1.dirty = true;
input2.dirty = true;
sameInput1.dirty = true;
const saveRes = await service.saveAll();
assert.equal(saveRes, true);
assert.equal(input1.gotSaved, true);
assert.equal(input2.gotSaved, true);
@ -820,6 +850,10 @@ suite('EditorService', () => {
input2.gotSavedAs = false;
input2.gotReverted = false;
input1.dirty = true;
input2.dirty = true;
sameInput1.dirty = true;
await service.saveAll({ saveAs: true });
assert.equal(input1.gotSavedAs, true);
@ -833,6 +867,48 @@ suite('EditorService', () => {
part.dispose();
});
test('saveAll, revertAll (sticky editor)', async function () {
const [part, service] = createEditorService();
const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID);
input1.dirty = true;
const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID);
input2.dirty = true;
const sameInput1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID);
sameInput1.dirty = true;
await part.whenRestored;
await service.openEditor(input1, { pinned: true, sticky: true });
await service.openEditor(input2, { pinned: true });
await service.openEditor(sameInput1, { pinned: true }, SIDE_GROUP);
const revertRes = await service.revertAll({ excludeSticky: true });
assert.equal(revertRes, true);
assert.equal(input1.gotReverted, false);
assert.equal(sameInput1.gotReverted, true);
input1.gotSaved = false;
input1.gotSavedAs = false;
input1.gotReverted = false;
sameInput1.gotSaved = false;
sameInput1.gotSavedAs = false;
sameInput1.gotReverted = false;
input1.dirty = true;
input2.dirty = true;
sameInput1.dirty = true;
const saveRes = await service.saveAll({ excludeSticky: true });
assert.equal(saveRes, true);
assert.equal(input1.gotSaved, false);
assert.equal(input2.gotSaved, true);
assert.equal(sameInput1.gotSaved, true);
part.dispose();
});
test('file delete closes editor', async function () {
return testFileDeleteEditorClose(false);
});

View file

@ -536,4 +536,37 @@ suite('EditorsObserver', function () {
observer.dispose();
part.dispose();
});
test('observer does not close sticky', async () => {
const part = await createPart();
part.enforcePartOptions({ limit: { enabled: true, value: 3 } });
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID);
const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, sticky: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
assert.equal(rootGroup.count, 3);
assert.equal(rootGroup.isOpened(input1), true);
assert.equal(rootGroup.isOpened(input2), false);
assert.equal(rootGroup.isOpened(input3), true);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), false);
assert.equal(observer.hasEditor(input3.resource), true);
assert.equal(observer.hasEditor(input4.resource), true);
observer.dispose();
part.dispose();
});
});

View file

@ -87,6 +87,7 @@ interface IStackEntry {
interface IRecentlyClosedFile {
resource: URI;
index: number;
sticky: boolean;
}
export class HistoryService extends Disposable implements IHistoryService {
@ -616,7 +617,7 @@ export class HistoryService extends Disposable implements IHistoryService {
// Remove all inputs matching and add as last recently closed
this.removeFromRecentlyClosedFiles(event.editor);
this.recentlyClosedFiles.push({ resource, index: event.index });
this.recentlyClosedFiles.push({ resource, index: event.index, sticky: event.sticky });
// Bounding
if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
@ -637,7 +638,10 @@ export class HistoryService extends Disposable implements IHistoryService {
if (lastClosedFile) {
(async () => {
const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options: { pinned: true, index: lastClosedFile.index } });
const editor = await this.editorService.openEditor({
resource: lastClosedFile.resource,
options: { pinned: true, sticky: lastClosedFile.sticky, index: lastClosedFile.index }
});
// Fix for https://github.com/Microsoft/vscode/issues/67882
// If opening of the editor fails, make sure to try the next one

View file

@ -41,6 +41,38 @@ function createGroup(serialized?: ISerializedEditorGroup): EditorGroup {
return inst().createInstance(EditorGroup, serialized);
}
function closeAllEditors(group: EditorGroup): void {
for (const editor of group.getEditors(EditorsOrder.SEQUENTIAL)) {
group.closeEditor(editor, false);
}
}
function closeEditors(group: EditorGroup, except: EditorInput, direction?: CloseDirection): void {
const index = group.indexOf(except);
if (index === -1) {
return; // not found
}
// Close to the left
if (direction === CloseDirection.LEFT) {
for (let i = index - 1; i >= 0; i--) {
group.closeEditor(group.getEditorByIndex(i)!);
}
}
// Close to the right
else if (direction === CloseDirection.RIGHT) {
for (let i = group.getEditors(EditorsOrder.SEQUENTIAL).length - 1; i > index; i--) {
group.closeEditor(group.getEditorByIndex(i)!);
}
}
// Both directions
else {
group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).filter(editor => !editor.matches(except)).forEach(editor => group.closeEditor(editor));
}
}
interface GroupEvents {
opened: EditorInput[];
activated: EditorInput[];
@ -206,13 +238,25 @@ suite('Workbench editor groups', () => {
group.openEditor(input2, { pinned: true, active: true });
group.openEditor(input3, { pinned: false, active: true });
// Sticky
group.stick(input2);
assert.ok(group.isSticky(input2));
const clone = group.clone();
assert.notEqual(group.id, clone.id);
assert.equal(clone.count, 3);
assert.equal(clone.isPinned(input1), true);
assert.equal(clone.isActive(input1), false);
assert.equal(clone.isSticky(input1), false);
assert.equal(clone.isPinned(input2), true);
assert.equal(clone.isActive(input2), false);
assert.equal(clone.isSticky(input2), true);
assert.equal(clone.isPinned(input3), false);
assert.equal(clone.isActive(input3), true);
assert.equal(clone.isSticky(input3), false);
});
test('contains()', function () {
@ -346,6 +390,61 @@ suite('Workbench editor groups', () => {
assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0);
});
test('group serialization (sticky editor)', function () {
inst().invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const group = createGroup();
const input1 = input();
const input2 = input();
const input3 = input();
// Case 1: inputs can be serialized and deserialized
group.openEditor(input1, { pinned: true, active: true });
group.openEditor(input2, { pinned: true, active: true });
group.openEditor(input3, { pinned: false, active: true });
group.stick(input2);
assert.ok(group.isSticky(input2));
let deserialized = createGroup(group.serialize());
assert.equal(group.id, deserialized.id);
assert.equal(deserialized.count, 3);
assert.equal(deserialized.isPinned(input1), true);
assert.equal(deserialized.isActive(input1), false);
assert.equal(deserialized.isSticky(input1), false);
assert.equal(deserialized.isPinned(input2), true);
assert.equal(deserialized.isActive(input2), false);
assert.equal(deserialized.isSticky(input2), true);
assert.equal(deserialized.isPinned(input3), false);
assert.equal(deserialized.isActive(input3), true);
assert.equal(deserialized.isSticky(input3), false);
// Case 2: inputs cannot be serialized
TestEditorInputFactory.disableSerialize = true;
deserialized = createGroup(group.serialize());
assert.equal(group.id, deserialized.id);
assert.equal(deserialized.count, 0);
assert.equal(deserialized.stickyCount, 0);
assert.equal(deserialized.getEditors(EditorsOrder.SEQUENTIAL).length, 0);
assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0);
// Case 3: inputs cannot be deserialized
TestEditorInputFactory.disableSerialize = false;
TestEditorInputFactory.disableDeserialize = true;
deserialized = createGroup(group.serialize());
assert.equal(group.id, deserialized.id);
assert.equal(deserialized.count, 0);
assert.equal(deserialized.stickyCount, 0);
assert.equal(deserialized.getEditors(EditorsOrder.SEQUENTIAL).length, 0);
assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0);
});
test('One Editor', function () {
const group = createGroup();
const events = groupListener(group);
@ -362,7 +461,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1);
assert.equal(group.activeEditor, input1);
assert.equal(group.isActive(input1), true);
assert.equal(group.isPreview(input1), false);
assert.equal(group.isPinned(input1), true);
assert.equal(group.isPinned(0), true);
@ -386,7 +484,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1);
assert.equal(group.activeEditor, input2);
assert.equal(group.isActive(input2), true);
assert.equal(group.isPreview(input2), true);
assert.equal(group.isPinned(input2), false);
assert.equal(group.isPinned(0), false);
@ -416,7 +513,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1);
assert.equal(group.activeEditor, input3);
assert.equal(group.isActive(input3), true);
assert.equal(group.isPreview(input3), false);
assert.equal(group.isPinned(input3), true);
assert.equal(group.isPinned(0), true);
@ -446,7 +542,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1);
assert.equal(group.activeEditor, input4);
assert.equal(group.isActive(input4), true);
assert.equal(group.isPreview(input4), true);
assert.equal(group.isPinned(input4), false);
assert.equal(group.isPinned(0), false);
@ -484,13 +579,10 @@ suite('Workbench editor groups', () => {
assert.equal(group.activeEditor, input3);
assert.equal(group.isActive(input1), false);
assert.equal(group.isPinned(input1), true);
assert.equal(group.isPreview(input1), false);
assert.equal(group.isActive(input2), false);
assert.equal(group.isPinned(input2), true);
assert.equal(group.isPreview(input2), false);
assert.equal(group.isActive(input3), true);
assert.equal(group.isPinned(input3), true);
assert.equal(group.isPreview(input3), false);
assert.equal(events.opened[0], input1);
assert.equal(events.opened[1], input2);
@ -523,7 +615,7 @@ suite('Workbench editor groups', () => {
group.closeEditor(sameInput1);
assert.equal(events.closed[0].editor, input1);
group.closeAllEditors();
closeAllEditors(group);
assert.equal(events.closed.length, 3);
assert.equal(group.count, 0);
@ -576,7 +668,7 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input1);
group.closeAllEditors();
closeAllEditors(group);
assert.equal(events.closed.length, 3);
assert.equal(group.count, 0);
@ -600,15 +692,13 @@ suite('Workbench editor groups', () => {
assert.equal(group.isActive(input1), true);
assert.equal(group.isPinned(input1), true);
assert.equal(group.isPinned(0), true);
assert.equal(group.isPreview(input1), false);
assert.equal(group.isActive(input2), false);
assert.equal(group.isPinned(input2), true);
assert.equal(group.isPinned(1), true);
assert.equal(group.isPreview(input2), false);
assert.equal(group.isActive(input3), false);
assert.equal(group.isPinned(input3), true);
assert.equal(group.isPinned(2), true);
assert.equal(group.isPreview(input3), false);
assert.equal(group.isPinned(input3), true);
const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
assert.equal(mru[0], input1);
@ -634,7 +724,7 @@ suite('Workbench editor groups', () => {
assert.equal(group.activeEditor, input3);
assert.equal(group.isActive(input3), true);
assert.equal(group.isPinned(input3), false);
assert.equal(group.isPreview(input3), true);
assert.equal(!group.isPinned(input3), true);
assert.equal(events.opened[0], input1);
assert.equal(events.opened[1], input2);
@ -703,7 +793,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.activeEditor, input3);
assert.equal(group.isPinned(input3), true);
assert.equal(group.isPreview(input3), false);
assert.equal(group.isActive(input3), true);
assert.equal(events.pinned[0], input3);
assert.equal(group.count, 3);
@ -712,7 +801,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.activeEditor, input3);
assert.equal(group.isPinned(input1), false);
assert.equal(group.isPreview(input1), true);
assert.equal(group.isActive(input1), false);
assert.equal(events.unpinned[0], input1);
assert.equal(group.count, 3);
@ -883,6 +971,25 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input3);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[3], input4);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input5);
assert.equal(events.moved.length, 4);
group.moveEditor(input1, 0);
assert.equal(events.moved.length, 4);
group.moveEditor(input1, -1);
assert.equal(events.moved.length, 4);
group.moveEditor(input5, 4);
assert.equal(events.moved.length, 4);
group.moveEditor(input5, 100);
assert.equal(events.moved.length, 4);
group.moveEditor(input5, -1);
assert.equal(events.moved.length, 5);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input5);
group.moveEditor(input1, 100);
assert.equal(events.moved.length, 6);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input1);
});
test('Multiple Editors - move editor across groups', function () {
@ -978,11 +1085,11 @@ suite('Workbench editor groups', () => {
group.openEditor(input5, { active: true, pinned: true });
// Close Others
group.closeEditors(group.activeEditor!);
closeEditors(group, group.activeEditor!);
assert.equal(group.activeEditor, input5);
assert.equal(group.count, 1);
group.closeAllEditors();
closeAllEditors(group);
group.openEditor(input1, { active: true, pinned: true });
group.openEditor(input2, { active: true, pinned: true });
group.openEditor(input3, { active: true, pinned: true });
@ -992,14 +1099,14 @@ suite('Workbench editor groups', () => {
// Close Left
assert.equal(group.activeEditor, input3);
group.closeEditors(group.activeEditor!, CloseDirection.LEFT);
closeEditors(group, group.activeEditor!, CloseDirection.LEFT);
assert.equal(group.activeEditor, input3);
assert.equal(group.count, 3);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input3);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input4);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input5);
group.closeAllEditors();
closeAllEditors(group);
group.openEditor(input1, { active: true, pinned: true });
group.openEditor(input2, { active: true, pinned: true });
group.openEditor(input3, { active: true, pinned: true });
@ -1009,7 +1116,7 @@ suite('Workbench editor groups', () => {
// Close Right
assert.equal(group.activeEditor, input3);
group.closeEditors(group.activeEditor!, CloseDirection.RIGHT);
closeEditors(group, group.activeEditor!, CloseDirection.RIGHT);
assert.equal(group.activeEditor, input3);
assert.equal(group.count, 3);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1);
@ -1053,7 +1160,7 @@ suite('Workbench editor groups', () => {
assert.equal(openedEditor, testJs);
assert.equal(group.previewEditor, styleCss);
assert.equal(group.activeEditor, testJs);
assert.equal(group.isPreview(styleCss), true);
assert.equal(group.isPinned(styleCss), false);
assert.equal(group.isPinned(testJs), true);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], styleCss);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], testJs);
@ -1064,7 +1171,7 @@ suite('Workbench editor groups', () => {
group.openEditor(indexHtml2, { active: true });
assert.equal(group.activeEditor, indexHtml2);
assert.equal(group.previewEditor, indexHtml2);
assert.equal(group.isPreview(indexHtml2), true);
assert.equal(group.isPinned(indexHtml2), false);
assert.equal(group.isPinned(testJs), true);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], testJs);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], indexHtml2);
@ -1081,14 +1188,12 @@ suite('Workbench editor groups', () => {
const indexHtml3 = input('index.html');
group.pin(indexHtml3);
assert.equal(group.isPinned(indexHtml3), true);
assert.equal(group.isPreview(indexHtml3), false);
assert.equal(group.activeEditor, testJs);
// [test.js, index.html] -> [test.js, file.ts, index.html]
const fileTs = input('file.ts');
group.openEditor(fileTs, { active: true, pinned: true });
assert.equal(group.isPinned(fileTs), true);
assert.equal(group.isPreview(fileTs), false);
assert.equal(group.count, 3);
assert.equal(group.activeEditor, fileTs);
@ -1096,7 +1201,6 @@ suite('Workbench editor groups', () => {
group.unpin(fileTs);
assert.equal(group.count, 3);
assert.equal(group.isPinned(fileTs), false);
assert.equal(group.isPreview(fileTs), true);
assert.equal(group.activeEditor, fileTs);
// [test.js, /file.ts/, index.html] -> [test.js, /other.ts/, index.html]
@ -1132,7 +1236,6 @@ suite('Workbench editor groups', () => {
assert.equal(group.activeEditor, testJs);
assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs));
assert.equal(group.isPinned(testJs), false);
assert.equal(group.isPreview(testJs), true);
// /test.js/ -> []
group.closeEditor(testJs);
@ -1289,6 +1392,41 @@ suite('Workbench editor groups', () => {
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(serializableInput1), true);
});
test('Single group, multiple editors - persist (some not persistable, sticky editors)', function () {
let inst = new TestInstantiationService();
inst.stub(IStorageService, new TestStorageService());
inst.stub(IWorkspaceContextService, new TestContextService());
const lifecycle = new TestLifecycleService();
inst.stub(ILifecycleService, lifecycle);
inst.stub(ITelemetryService, NullTelemetryService);
const config = new TestConfigurationService();
config.setUserConfiguration('workbench', { editor: { openPositioning: 'right' } });
inst.stub(IConfigurationService, config);
inst.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
let group = createGroup();
const serializableInput1 = input();
const nonSerializableInput2 = input('3', true);
const serializableInput2 = input();
group.openEditor(serializableInput1, { active: true, pinned: true });
group.openEditor(nonSerializableInput2, { active: true, pinned: true, sticky: true });
group.openEditor(serializableInput2, { active: false, pinned: true });
assert.equal(group.count, 3);
assert.equal(group.stickyCount, 1);
// Create model again - should load from storage
group = inst.createInstance(EditorGroup, group.serialize());
assert.equal(group.count, 2);
assert.equal(group.stickyCount, 0);
});
test('Multiple groups, multiple editors - persist (some not persistable, causes empty group)', function () {
let inst = new TestInstantiationService();
@ -1413,7 +1551,7 @@ suite('Workbench editor groups', () => {
assert.equal(dirty2Counter, 1);
assert.equal(label2ChangeCounter, 1);
group2.closeAllEditors();
closeAllEditors(group2);
(<TestEditorInput>input2).setDirty();
(<TestEditorInput>input2).setLabel();
@ -1423,4 +1561,268 @@ suite('Workbench editor groups', () => {
assert.equal(dirty1Counter, 1);
assert.equal(label1ChangeCounter, 1);
});
test('Sticky Editors', function () {
const group = createGroup();
const input1 = input();
const input2 = input();
const input3 = input();
const input4 = input();
group.openEditor(input1, { pinned: true, active: true });
group.openEditor(input2, { pinned: true, active: true });
group.openEditor(input3, { pinned: false, active: true });
assert.equal(group.stickyCount, 0);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 3);
assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 3);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 3);
assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 3);
// Stick last editor should move it first and pin
group.stick(input3);
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input1), false);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), true);
assert.equal(group.isPinned(input3), true);
assert.equal(group.indexOf(input1), 1);
assert.equal(group.indexOf(input2), 2);
assert.equal(group.indexOf(input3), 0);
let sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL);
assert.equal(sequentialAllEditors.length, 3);
let sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true });
assert.equal(sequentialEditorsExcludingSticky.length, 2);
assert.ok(sequentialEditorsExcludingSticky.indexOf(input1) >= 0);
assert.ok(sequentialEditorsExcludingSticky.indexOf(input2) >= 0);
let mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
assert.equal(mruAllEditors.length, 3);
let mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true });
assert.equal(mruEditorsExcludingSticky.length, 2);
assert.ok(mruEditorsExcludingSticky.indexOf(input1) >= 0);
assert.ok(mruEditorsExcludingSticky.indexOf(input2) >= 0);
// Sticking same editor again is a no-op
group.stick(input3);
assert.equal(group.isSticky(input3), true);
// Sticking last editor now should move it after sticky one
group.stick(input2);
assert.equal(group.stickyCount, 2);
assert.equal(group.isSticky(input1), false);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), true);
assert.equal(group.indexOf(input1), 2);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 0);
sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL);
assert.equal(sequentialAllEditors.length, 3);
sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true });
assert.equal(sequentialEditorsExcludingSticky.length, 1);
assert.ok(sequentialEditorsExcludingSticky.indexOf(input1) >= 0);
mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
assert.equal(mruAllEditors.length, 3);
mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true });
assert.equal(mruEditorsExcludingSticky.length, 1);
assert.ok(mruEditorsExcludingSticky.indexOf(input1) >= 0);
// Sticking remaining editor also works
group.stick(input1);
assert.equal(group.stickyCount, 3);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), true);
assert.equal(group.indexOf(input1), 2);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 0);
sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL);
assert.equal(sequentialAllEditors.length, 3);
sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true });
assert.equal(sequentialEditorsExcludingSticky.length, 0);
mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
assert.equal(mruAllEditors.length, 3);
mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true });
assert.equal(mruEditorsExcludingSticky.length, 0);
// Unsticking moves editor after sticky ones
group.unstick(input3);
assert.equal(group.stickyCount, 2);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 1);
assert.equal(group.indexOf(input2), 0);
assert.equal(group.indexOf(input3), 2);
// Unsticking all works
group.unstick(input1);
group.unstick(input2);
assert.equal(group.stickyCount, 0);
assert.equal(group.isSticky(input1), false);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), false);
group.moveEditor(input1, 0);
group.moveEditor(input2, 1);
group.moveEditor(input3, 2);
// Opening a new editor always opens after sticky editors
group.stick(input1);
group.stick(input2);
group.setActive(input1);
const events = groupListener(group);
group.openEditor(input4, { pinned: true, active: true });
assert.equal(group.indexOf(input4), 2);
group.closeEditor(input4);
assert.equal(events.closed[0].sticky, false);
group.setActive(input2);
group.openEditor(input4, { pinned: true, active: true });
assert.equal(group.indexOf(input4), 2);
group.closeEditor(input4);
assert.equal(events.closed[1].sticky, false);
// Reset
assert.equal(group.stickyCount, 2);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 2);
// Moving a sticky editor works
group.moveEditor(input1, 1); // still moved within sticky range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 1);
assert.equal(group.indexOf(input2), 0);
assert.equal(group.indexOf(input3), 2);
group.moveEditor(input1, 0); // still moved within sticky range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 2);
group.moveEditor(input1, 2); // moved out of sticky range
assert.equal(group.isSticky(input1), false);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 2);
assert.equal(group.indexOf(input2), 0);
assert.equal(group.indexOf(input3), 1);
group.moveEditor(input2, 2); // moved out of sticky range
assert.equal(group.isSticky(input1), false);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 1);
assert.equal(group.indexOf(input2), 2);
assert.equal(group.indexOf(input3), 0);
// Reset
group.moveEditor(input1, 0);
group.moveEditor(input2, 1);
group.moveEditor(input3, 2);
group.stick(input1);
group.unstick(input2);
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 2);
// Moving a unsticky editor in works
group.moveEditor(input3, 1); // still moved within unsticked range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 2);
assert.equal(group.indexOf(input3), 1);
group.moveEditor(input3, 2); // still moved within unsticked range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), false);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 2);
group.moveEditor(input3, 0); // moved into sticky range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), false);
assert.equal(group.isSticky(input3), true);
assert.equal(group.indexOf(input1), 1);
assert.equal(group.indexOf(input2), 2);
assert.equal(group.indexOf(input3), 0);
group.moveEditor(input2, 0); // moved into sticky range
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), true);
assert.equal(group.indexOf(input1), 2);
assert.equal(group.indexOf(input2), 0);
assert.equal(group.indexOf(input3), 1);
// Closing a sticky editor updates state properly
group.stick(input1);
group.stick(input2);
group.unstick(input3);
assert.equal(group.stickyCount, 2);
group.closeEditor(input1);
assert.equal(events.closed[2].sticky, true);
assert.equal(group.stickyCount, 1);
group.closeEditor(input2);
assert.equal(events.closed[3].sticky, true);
assert.equal(group.stickyCount, 0);
closeAllEditors(group);
assert.equal(group.stickyCount, 0);
// Open sticky
group.openEditor(input1, { sticky: true });
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input1), true);
group.openEditor(input2, { pinned: true, active: true });
assert.equal(group.stickyCount, 1);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), false);
group.openEditor(input2, { sticky: true });
assert.equal(group.stickyCount, 2);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
group.openEditor(input3, { pinned: true, active: true });
group.openEditor(input4, { pinned: false, active: true, sticky: true });
assert.equal(group.stickyCount, 3);
assert.equal(group.isSticky(input1), true);
assert.equal(group.isSticky(input2), true);
assert.equal(group.isSticky(input3), false);
assert.equal(group.isSticky(input4), true);
assert.equal(group.isPinned(input4), true);
assert.equal(group.indexOf(input1), 0);
assert.equal(group.indexOf(input2), 1);
assert.equal(group.indexOf(input3), 3);
assert.equal(group.indexOf(input4), 2);
});
});

View file

@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor';
import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor';
import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
@ -51,7 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverrideHandler, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditorInputType, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, IOpenEditorOverrideEntry, ICustomEditorViewTypesHandler } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor';
@ -340,8 +340,6 @@ export class TestHistoryService implements IHistoryService {
openLastEditLocation(): void { }
}
export class TestFileDialogService implements IFileDialogService {
_serviceBrand: undefined;
@ -547,6 +545,7 @@ export class TestEditorGroupView implements IEditorGroupView {
activeEditor!: IEditorInput;
previewEditor!: IEditorInput;
count!: number;
stickyCount!: number;
disposed!: boolean;
editors: ReadonlyArray<IEditorInput> = [];
label!: string;
@ -578,14 +577,17 @@ export class TestEditorGroupView implements IEditorGroupView {
openEditors(_editors: IEditorInputWithOptions[]): Promise<IEditorPane> { throw new Error('not implemented'); }
isOpened(_editor: IEditorInput | IResourceEditorInput): boolean { return false; }
isPinned(_editor: IEditorInput): boolean { return false; }
isSticky(_editor: IEditorInput): boolean { return false; }
isActive(_editor: IEditorInput): boolean { return false; }
moveEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IMoveEditorOptions): void { }
copyEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: ICopyEditorOptions): void { }
closeEditor(_editor?: IEditorInput, options?: ICloseEditorOptions): Promise<void> { return Promise.resolve(); }
closeEditors(_editors: IEditorInput[] | { except?: IEditorInput; direction?: CloseDirection; savedOnly?: boolean; }, options?: ICloseEditorOptions): Promise<void> { return Promise.resolve(); }
closeAllEditors(): Promise<void> { return Promise.resolve(); }
closeEditors(_editors: IEditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<void> { return Promise.resolve(); }
closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { return Promise.resolve(); }
replaceEditors(_editors: IEditorReplacement[]): Promise<void> { return Promise.resolve(); }
pinEditor(_editor?: IEditorInput): void { }
stickEditor(editor?: IEditorInput | undefined): void { }
unstickEditor(editor?: IEditorInput | undefined): void { }
focus(): void { }
invokeWithinContext<T>(fn: (accessor: ServicesAccessor) => T): T { throw new Error('not implemented'); }
setActive(_isActive: boolean): void { }
@ -666,8 +668,8 @@ export class TestEditorService implements EditorServiceImpl {
createEditorInput(_input: IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput): EditorInput { throw new Error('not implemented'); }
save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
saveAll(options?: ISaveEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise<void> { throw new Error('Method not implemented.'); }
revertAll(options?: IRevertAllEditorsOptions): Promise<void> { throw new Error('Method not implemented.'); }
revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
}
export class TestFileService implements IFileService {
@ -1061,6 +1063,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput
}
async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
this.gotSaved = true;
this.dirty = false;
return this;
}
async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
@ -1071,6 +1074,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput
this.gotReverted = true;
this.gotSaved = false;
this.gotSavedAs = false;
this.dirty = false;
}
setDirty(): void { this.dirty = true; }
isDirty(): boolean {