Merge from vscode 7653d836944892f83ce9e1f95c1204bafa1aec31

This commit is contained in:
ADS Merger
2020-05-08 03:58:34 +00:00
parent dac1970c43
commit fa62ec1f34
209 changed files with 5131 additions and 2480 deletions

View File

@@ -202,8 +202,9 @@ class NavigateDownAction extends BaseNavigationAction {
}
function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Parts, next: boolean): Parts {
const neighbour = part === Parts.EDITOR_PART ? (next ? Parts.STATUSBAR_PART : Parts.PANEL_PART) : part === Parts.STATUSBAR_PART ? (next ? Parts.SIDEBAR_PART : Parts.EDITOR_PART) :
part === Parts.SIDEBAR_PART ? (next ? Parts.PANEL_PART : Parts.STATUSBAR_PART) : part === Parts.PANEL_PART ? (next ? Parts.EDITOR_PART : Parts.SIDEBAR_PART) : Parts.EDITOR_PART;
const neighbour = part === Parts.EDITOR_PART ? (next ? Parts.STATUSBAR_PART : Parts.PANEL_PART) : part === Parts.STATUSBAR_PART ? (next ? Parts.ACTIVITYBAR_PART : Parts.EDITOR_PART) :
part === Parts.ACTIVITYBAR_PART ? (next ? Parts.SIDEBAR_PART : Parts.STATUSBAR_PART) : part === Parts.SIDEBAR_PART ? (next ? Parts.PANEL_PART : Parts.ACTIVITYBAR_PART) :
part === Parts.PANEL_PART ? (next ? Parts.EDITOR_PART : Parts.SIDEBAR_PART) : Parts.EDITOR_PART;
if (layoutService.isVisible(neighbour) || neighbour === Parts.EDITOR_PART) {
return neighbour;
}
@@ -212,8 +213,8 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part
}
function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, next: boolean): void {
const currentlyFocusedPart = layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART :
layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined;
const currentlyFocusedPart = layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART :
layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined;
let partToFocus = Parts.EDITOR_PART;
if (currentlyFocusedPart) {
partToFocus = findVisibleNeighbour(layoutService, currentlyFocusedPart, next);

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); {{SQL CARBON EDIT}} we don't want to special case untitled files
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

@@ -177,6 +177,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
private backupFileService!: IBackupFileService;
private notificationService!: INotificationService;
private themeService!: IThemeService;
private activityBarService!: IActivityBarService;
protected readonly state = {
fullscreen: false,
@@ -260,8 +261,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
this.viewDescriptorService = accessor.get(IViewDescriptorService);
this.titleService = accessor.get(ITitleService);
this.notificationService = accessor.get(INotificationService);
this.activityBarService = accessor.get(IActivityBarService);
accessor.get(IStatusbarService); // not used, but called to ensure instantiated
accessor.get(IActivityBarService); // not used, but called to ensure instantiated
// Listeners
this.registerLayoutListeners();
@@ -846,6 +847,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
activeViewlet.focus();
}
break;
case Parts.ACTIVITYBAR_PART:
this.activityBarService.focusActivityBar();
break;
default:
// Status Bar, Activity Bar and Title Bar simply pass focus to container
const container = this.getContainer(part);

View File

@@ -10,7 +10,6 @@
.monaco-workbench .part > .drop-block-overlay.visible {
visibility: visible;
opacity: 1;
}
.monaco-workbench .part > .drop-block-overlay {
@@ -18,10 +17,8 @@
top: 0;
width: 100%;
height: 100%;
backdrop-filter: brightness(97%) blur(2px);
visibility: hidden;
opacity: 0;
transition: opacity .5s, visibility .5s;
z-index: 10;
}

View File

@@ -168,6 +168,10 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.registerListeners();
}
focusActivityBar(): void {
this.compositeBar.focus();
}
private registerListeners(): void {
// View Container Changes

View File

@@ -66,8 +66,6 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop {
if (targetCompositeId) {
this.moveComposite(currentContainer.id, targetCompositeId, before);
}
this.openComposite(currentContainer.id, true);
}
}
@@ -255,6 +253,12 @@ export class CompositeBar extends Widget implements ICompositeBar {
return actionBarDiv;
}
focus(): void {
if (this.compositeSwitcherBar) {
this.compositeSwitcherBar.focus();
}
}
layout(dimension: Dimension): void {
this.dimension = dimension;
if (dimension.height === 0 || dimension.width === 0) {
@@ -275,8 +279,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
// Add to the model
if (this.model.add(id, name, order)) {
this.computeSizes([this.model.findItem(id)]);
// Set timeout helps prevent flicker
setTimeout(() => this.updateCompositeSwitcher(), 0);
this.updateCompositeSwitcher();
}
}

View File

@@ -548,11 +548,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
if (e.eventData.dataTransfer) {
e.eventData.dataTransfer.effectAllowed = 'move';
}
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
// Remove focus indicator when dragging
this.blur();
}
}));

View File

@@ -19,6 +19,7 @@ import { MementoObject } from 'vs/workbench/common/memento';
import { isEqualOrParent, joinPath } from 'vs/base/common/resources';
import { isLinux } from 'vs/base/common/platform';
import { indexOfPath } from 'vs/base/common/extpath';
import { IDisposable } from 'vs/base/common/lifecycle';
/**
* The base class of editors in the workbench. Editors register themselves for specific editor inputs.
@@ -171,6 +172,7 @@ interface MapGroupToMemento<T> {
export class EditorMemento<T> implements IEditorMemento<T> {
private cache: LRUCache<string, MapGroupToMemento<T>> | undefined;
private cleanedUp = false;
private editorDisposables: Map<EditorInput, IDisposable> | undefined;
constructor(
public readonly id: string,
@@ -200,9 +202,18 @@ export class EditorMemento<T> implements IEditorMemento<T> {
// Automatically clear when editor input gets disposed if any
if (resourceOrEditor instanceof EditorInput) {
Event.once(resourceOrEditor.onDispose)(() => {
this.clearEditorState(resource);
});
const editor = resourceOrEditor;
if (!this.editorDisposables) {
this.editorDisposables = new Map<EditorInput, IDisposable>();
}
if (!this.editorDisposables.has(editor)) {
this.editorDisposables.set(editor, Event.once(resourceOrEditor.onDispose)(() => {
this.clearEditorState(resource);
this.editorDisposables?.delete(editor);
}));
}
}
}

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

@@ -48,7 +48,7 @@ export class EditorsObserver extends Disposable {
}
get editors(): IEditorIdentifier[] {
return [...this.mostRecentEditorsMap.values()];
return this.mostRecentEditorsMap.values();
}
hasEditor(resource: URI): boolean {
@@ -283,7 +283,7 @@ export class EditorsObserver extends Disposable {
// Across all editor groups
else {
await this.doEnsureOpenedEditorsLimit(limit, [...this.mostRecentEditorsMap.values()], exclude);
await this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude);
}
}
@@ -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;
});
@@ -342,7 +346,7 @@ export class EditorsObserver extends Disposable {
private serialize(): ISerializedEditorsList {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const entries = [...this.mostRecentEditorsMap.values()];
const entries = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {

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';
@@ -67,6 +67,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;
@@ -392,10 +397,6 @@ export class TabsTitleControl extends TitleControl {
this.handleClosedEditors();
}
closeAllEditors(): void {
this.handleClosedEditors();
}
private handleClosedEditors(): void {
// There are tabs to show
@@ -453,7 +454,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 {
@@ -487,7 +505,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
@@ -745,10 +763,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');
@@ -1010,7 +1025,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));
@@ -1018,10 +1033,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}`);
});
@@ -1036,13 +1053,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) {
@@ -1056,7 +1097,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 }
);
this.setEditorTabColor(editor, tabContainer, this.group.isActive(editor)); // {{SQL CARBON EDIT}} -- Display the editor's tab color
@@ -1173,8 +1214,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;
}
@@ -1192,20 +1233,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;
@@ -1217,42 +1304,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;
@@ -1494,11 +1617,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;
}
`;
@@ -1521,19 +1644,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

@@ -181,7 +181,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 {
@@ -237,7 +242,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 Source EULA. 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());
@@ -86,6 +88,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));
@@ -222,6 +225,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;
@@ -266,10 +270,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);
}
}
@@ -295,6 +297,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
@@ -302,6 +329,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;
@@ -325,6 +354,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();
@@ -351,12 +381,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

@@ -207,7 +207,8 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
}
if (isActive) {
if (!activePanel) {
// Only try to open the panel if it has been created and visible
if (!activePanel && this.element && this.layoutService.isVisible(Parts.PANEL_PART)) {
this.doOpenPanel(panel.id);
}

View File

@@ -22,7 +22,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { isThemeColor } from 'vs/editor/common/editorCommon';
import { Color } from 'vs/base/common/color';
import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, removeClass, EventType, hide, show, removeClasses } from 'vs/base/browser/dom';
import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, removeClass, EventType, hide, show, removeClasses, isAncestor } from 'vs/base/browser/dom';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@@ -35,6 +35,11 @@ import { assertIsDefined } from 'vs/base/common/types';
import { Emitter } from 'vs/base/common/event';
import { Command } from 'vs/editor/common/modes';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
interface IPendingStatusbarEntry {
id: string;
@@ -51,8 +56,11 @@ interface IStatusbarViewModelEntry {
alignment: StatusbarAlignment;
priority: number;
container: HTMLElement;
labelContainer: HTMLElement;
}
const CONTEXT_STATUS_BAR_FOCUSED = new RawContextKey<boolean>('statusBarFocused', false);
class StatusbarViewModel extends Disposable {
static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden';
@@ -188,6 +196,40 @@ class StatusbarViewModel extends Disposable {
return this._entries.filter(entry => entry.alignment === alignment);
}
focusNextEntry(): void {
this.focusEntry(+1, 0);
}
focusPreviousEntry(): void {
this.focusEntry(-1, this.entries.length - 1);
}
private focusEntry(delta: number, restartPosition: number): void {
const getVisibleEntry = (start: number) => {
let indexToFocus = start;
let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
while (entry && this.isHidden(entry.id)) {
indexToFocus += delta;
entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
}
return entry;
};
const focused = this._entries.find(entry => isAncestor(document.activeElement, entry.container));
if (focused) {
const entry = getVisibleEntry(this._entries.indexOf(focused) + delta);
if (entry) {
entry.labelContainer.focus();
return;
}
}
const entry = getVisibleEntry(restartPosition);
if (entry) {
entry.labelContainer.focus();
}
}
private updateVisibility(id: string, trigger: boolean): void;
private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void;
private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void {
@@ -355,6 +397,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
@IStorageService private readonly storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
@@ -415,7 +458,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
this.appendOneStatusbarEntry(itemContainer, alignment, priority);
// Add to view model
const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer };
const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer, labelContainer: item.labelContainer };
const viewModelEntryDispose = this.viewModel.add(viewModelEntry);
return {
@@ -442,9 +485,21 @@ export class StatusbarPart extends Part implements IStatusbarService {
}
}
focusNextEntry(): void {
this.viewModel.focusNextEntry();
}
focusPreviousEntry(): void {
this.viewModel.focusPreviousEntry();
}
createContentArea(parent: HTMLElement): HTMLElement {
this.element = parent;
// Track focus within container
const scopedContextKeyService = this.contextKeyService.createScoped(this.element);
CONTEXT_STATUS_BAR_FOCUSED.bindTo(scopedContextKeyService).set(true);
// Left items container
this.leftItemsContainer = document.createElement('div');
addClasses(this.leftItemsContainer, 'left-items', 'items-container');
@@ -645,13 +700,14 @@ class StatusbarEntryItem extends Disposable {
private entry!: IStatusbarEntry;
private labelContainer!: HTMLElement;
labelContainer!: HTMLElement;
private label!: CodiconLabel;
private readonly foregroundListener = this._register(new MutableDisposable());
private readonly backgroundListener = this._register(new MutableDisposable());
private readonly commandListener = this._register(new MutableDisposable());
private readonly commandMouseListener = this._register(new MutableDisposable());
private readonly commandKeyboardListener = this._register(new MutableDisposable());
constructor(
private container: HTMLElement,
@@ -711,11 +767,18 @@ class StatusbarEntryItem extends Disposable {
// Update: Command
if (!this.entry || entry.command !== this.entry.command) {
this.commandListener.clear();
this.commandMouseListener.clear();
this.commandKeyboardListener.clear();
const command = entry.command;
if (command) {
this.commandListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command));
this.commandMouseListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command));
this.commandKeyboardListener.value = addDisposableListener(this.labelContainer, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
this.executeCommand(command);
}
});
removeClass(this.labelContainer, 'disabled');
} else {
@@ -814,7 +877,8 @@ class StatusbarEntryItem extends Disposable {
dispose(this.foregroundListener);
dispose(this.backgroundListener);
dispose(this.commandListener);
dispose(this.commandMouseListener);
dispose(this.commandKeyboardListener);
}
}
@@ -822,6 +886,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
if (statusBarItemHoverBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:hover { background-color: ${statusBarItemHoverBackground}; }`);
collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus { background-color: ${statusBarItemHoverBackground}; }`);
}
const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
@@ -846,3 +911,27 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
});
registerSingleton(IStatusbarService, StatusbarPart);
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.statusBar.focusPrevious',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.LeftArrow,
secondary: [KeyCode.UpArrow],
when: CONTEXT_STATUS_BAR_FOCUSED,
handler: (accessor: ServicesAccessor) => {
const statusBarService = accessor.get(IStatusbarService);
statusBarService.focusPreviousEntry();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'workbench.statusBar.focusNext',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.RightArrow,
secondary: [KeyCode.DownArrow],
when: CONTEXT_STATUS_BAR_FOCUSED,
handler: (accessor: ServicesAccessor) => {
const statusBarService = accessor.get(IStatusbarService);
statusBarService.focusNextEntry();
}
});

View File

@@ -934,17 +934,21 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] {
const result: IAction[] = [];
let showHide = true;
if (!viewDescriptor && this.isViewMergedWithContainer()) {
viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.panes[0].id) || undefined;
showHide = false;
}
if (viewDescriptor) {
result.push(<IAction>{
id: `${viewDescriptor.id}.removeView`,
label: nls.localize('hideView', "Hide"),
enabled: viewDescriptor.canToggleVisibility,
run: () => this.toggleViewVisibility(viewDescriptor!.id)
});
if (showHide) {
result.push(<IAction>{
id: `${viewDescriptor.id}.removeView`,
label: nls.localize('hideView', "Hide"),
enabled: viewDescriptor.canToggleVisibility,
run: () => this.toggleViewVisibility(viewDescriptor!.id)
});
}
const view = this.getView(viewDescriptor.id);
if (view) {
result.push(...view.getContextMenuActions());
@@ -955,7 +959,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
id: `${viewDescriptor.id}.toggleVisibility`,
label: viewDescriptor.name,
checked: this.viewContainerModel.isVisible(viewDescriptor.id),
enabled: viewDescriptor.canToggleVisibility,
enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1),
run: () => this.toggleViewVisibility(viewDescriptor.id)
}));

View File

@@ -127,15 +127,10 @@ export class ViewsService extends Disposable implements IViewsService {
this.deregisterViewletOrPanel(container, location);
}
for (const { container, location } of added) {
this.registerViewletOrPanel(container, location);
this.onDidRegisterViewContainer(container, location);
}
}
private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void {
this.deregisterViewletOrPanel(viewContainer, from);
this.registerViewletOrPanel(viewContainer, to);
}
private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void {
this.registerViewletOrPanel(viewContainer, viewContainerLocation);
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
@@ -146,6 +141,11 @@ export class ViewsService extends Disposable implements IViewsService {
}));
}
private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void {
this.deregisterViewletOrPanel(viewContainer, from);
this.registerViewletOrPanel(viewContainer, to);
}
private onViewDescriptorsAdded(views: ReadonlyArray<IViewDescriptor>, container: ViewContainer): void {
const location = this.viewDescriptorService.getViewContainerLocation(container);
if (location === null) {

View File

@@ -71,7 +71,7 @@ class BrowserMain extends Disposable {
services.storageService.store(Settings.WORKSPACE_FIRST_OPEN, !(firstOpen ?? false), StorageScope.WORKSPACE);
}
{ await domContentLoaded(); }
await domContentLoaded();
mark('willStartWorkbench');
// Base Theme
@@ -272,7 +272,7 @@ class BrowserMain extends Disposable {
if (!this.configuration.userDataProvider) {
const remoteUserDataUri = this.getRemoteUserDataUri();
if (remoteUserDataUri) {
this.configuration.userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService));
this.configuration.userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService, logService));
}
}
}

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': {