Refresh master with initial release/0.24 snapshot (#332)

* Initial port of release/0.24 source code

* Fix additional headers

* Fix a typo in launch.json
This commit is contained in:
Karl Burtram
2017-12-15 15:38:57 -08:00
committed by GitHub
parent 271b3a0b82
commit 6ad0df0e3e
7118 changed files with 107999 additions and 56466 deletions

View File

@@ -6,75 +6,22 @@
'use strict';
import 'vs/css!./media/activityaction';
import nls = require('vs/nls');
import DOM = require('vs/base/browser/dom');
import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { Action } from 'vs/base/common/actions';
import { BaseActionItem, Separator, IBaseActionItemOptions } from 'vs/base/browser/ui/actionbar/actionbar';
import { IActivityBarService, ProgressBadge, TextBadge, NumberBadge, IconBadge, IBadge } from 'vs/workbench/services/activity/common/activityBarService';
import Event, { Emitter } from 'vs/base/common/event';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IActivity, IGlobalActivity } from 'vs/workbench/browser/activity';
import { IActivity, IGlobalActivity } from 'vs/workbench/common/activity';
import { dispose } from 'vs/base/common/lifecycle';
import { IViewletService, } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { contrastBorder, activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface IViewletActivity {
badge: IBadge;
clazz: string;
}
export class ActivityAction extends Action {
private badge: IBadge;
private _onDidChangeBadge = new Emitter<this>();
constructor(private _activity: IActivity) {
super(_activity.id, _activity.name, _activity.cssClass);
this.badge = null;
}
public get activity(): IActivity {
return this._activity;
}
public get onDidChangeBadge(): Event<this> {
return this._onDidChangeBadge.event;
}
public activate(): void {
if (!this.checked) {
this._setChecked(true);
}
}
public deactivate(): void {
if (this.checked) {
this._setChecked(false);
}
}
public getBadge(): IBadge {
return this.badge;
}
public setBadge(badge: IBadge): void {
this.badge = badge;
this._onDidChangeBadge.fire(this);
}
}
import { ActivityAction, ActivityActionItem, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
export class ViewletActivityAction extends ActivityAction {
@@ -83,14 +30,14 @@ export class ViewletActivityAction extends ActivityAction {
private lastRun: number = 0;
constructor(
private viewlet: ViewletDescriptor,
activity: IActivity,
@IViewletService private viewletService: IViewletService,
@IPartService private partService: IPartService
) {
super(viewlet);
super(activity);
}
public run(event): TPromise<any> {
public run(event: any): TPromise<any> {
if (event instanceof MouseEvent && event.button === 2) {
return TPromise.as(false); // do not run on right click
}
@@ -106,483 +53,15 @@ export class ViewletActivityAction extends ActivityAction {
const activeViewlet = this.viewletService.getActiveViewlet();
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) {
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.activity.id) {
return this.partService.setSideBarHidden(true);
}
return this.viewletService.openViewlet(this.viewlet.id, true)
.then(() => this.activate());
return this.viewletService.openViewlet(this.activity.id, true).then(() => this.activate());
}
}
export class ActivityActionItem extends BaseActionItem {
protected $container: Builder;
protected $label: Builder;
protected $badge: Builder;
private $badgeContent: Builder;
private mouseUpTimeout: number;
constructor(
action: ActivityAction,
options: IBaseActionItemOptions,
@IThemeService protected themeService: IThemeService
) {
super(null, action, options);
this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose);
action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose);
}
protected get activity(): IActivity {
return (this._action as ActivityAction).activity;
}
protected updateStyles(): void {
const theme = this.themeService.getTheme();
// Label
if (this.$label) {
const background = theme.getColor(ACTIVITY_BAR_FOREGROUND);
this.$label.style('background-color', background ? background.toString() : null);
}
// Badge
if (this.$badgeContent) {
const badgeForeground = theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND);
const badgeBackground = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND);
const contrastBorderColor = theme.getColor(contrastBorder);
this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null);
this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null);
this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null);
this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null);
this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null);
}
}
public render(container: HTMLElement): void {
super.render(container);
// Make the container tab-able for keyboard navigation
this.$container = $(container).attr({
tabIndex: '0',
role: 'button',
title: this.activity.name
});
// Try hard to prevent keyboard only focus feedback when using mouse
this.$container.on(DOM.EventType.MOUSE_DOWN, () => {
this.$container.addClass('clicked');
});
this.$container.on(DOM.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$container.removeClass('clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
// Label
this.$label = $('a.action-label').appendTo(this.builder);
if (this.activity.cssClass) {
this.$label.addClass(this.activity.cssClass);
}
this.$badge = this.builder.clone().div({ 'class': 'badge' }, (badge: Builder) => {
this.$badgeContent = badge.div({ 'class': 'badge-content' });
});
this.$badge.hide();
this.updateStyles();
}
private onThemeChange(theme: ITheme): void {
this.updateStyles();
}
public setBadge(badge: IBadge): void {
this.updateBadge(badge);
}
protected updateBadge(badge: IBadge): void {
this.$badgeContent.empty();
this.$badge.hide();
if (badge) {
// Number
if (badge instanceof NumberBadge) {
if (badge.number) {
this.$badgeContent.text(badge.number > 99 ? '99+' : badge.number.toString());
this.$badge.show();
}
}
// Text
else if (badge instanceof TextBadge) {
this.$badgeContent.text(badge.text);
this.$badge.show();
}
// Text
else if (badge instanceof IconBadge) {
this.$badge.show();
}
// Progress
else if (badge instanceof ProgressBadge) {
this.$badge.show();
}
const description = badge.getDescription();
this.$label.attr('aria-label', `${this.activity.name} - ${description}`);
this.$label.title(description);
}
}
private handleBadgeChangeEvenet(): void {
const action = this.getAction();
if (action instanceof ActivityAction) {
this.updateBadge(action.getBadge());
}
}
public dispose(): void {
super.dispose();
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
}
}
export class ViewletActionItem extends ActivityActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static toggleViewletPinnedAction: ToggleViewletPinnedAction;
private static draggedViewlet: ViewletDescriptor;
private _keybinding: string;
private cssClass: string;
constructor(
private action: ViewletActivityAction,
@IContextMenuService private contextMenuService: IContextMenuService,
@IActivityBarService private activityBarService: IActivityBarService,
@IKeybindingService private keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super(action, { draggable: true }, themeService);
this.cssClass = action.class;
this._keybinding = this.getKeybindingLabel(this.viewlet.id);
if (!ViewletActionItem.manageExtensionAction) {
ViewletActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction);
}
if (!ViewletActionItem.toggleViewletPinnedAction) {
ViewletActionItem.toggleViewletPinnedAction = instantiationService.createInstance(ToggleViewletPinnedAction, void 0);
}
}
private get viewlet(): ViewletDescriptor {
return this.action.activity as ViewletDescriptor;
}
private getKeybindingLabel(id: string): string {
const kb = this.keybindingService.lookupKeybinding(id);
if (kb) {
return kb.getLabel();
}
return null;
}
public render(container: HTMLElement): void {
super.render(container);
this.$container.on('contextmenu', e => {
DOM.EventHelper.stop(e, true);
this.showContextMenu(container);
});
// Allow to drag
this.$container.on(DOM.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
this.setDraggedViewlet(this.viewlet);
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
});
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
this.$container.on(DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
counter++;
this.updateFromDragging(container, true);
}
});
// Drag leave
this.$container.on(DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
counter--;
if (counter === 0) {
this.updateFromDragging(container, false);
}
}
});
// Drag end
this.$container.on(DOM.EventType.DRAG_END, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
counter = 0;
this.updateFromDragging(container, false);
ViewletActionItem.clearDraggedViewlet();
}
});
// Drop
this.$container.on(DOM.EventType.DROP, (e: DragEvent) => {
DOM.EventHelper.stop(e, true);
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
this.updateFromDragging(container, false);
ViewletActionItem.clearDraggedViewlet();
this.activityBarService.move(draggedViewlet.id, this.viewlet.id);
}
});
// Keybinding
this.keybinding = this._keybinding; // force update
// Activate on drag over to reveal targets
[this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!ViewletActionItem.getDraggedViewlet() && !this.getAction().checked) {
this.getAction().run();
}
}));
this.updateStyles();
}
private updateFromDragging(element: HTMLElement, isDragging: boolean): void {
const theme = this.themeService.getTheme();
const dragBackground = theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND);
element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null;
}
public static getDraggedViewlet(): ViewletDescriptor {
return ViewletActionItem.draggedViewlet;
}
private setDraggedViewlet(viewlet: ViewletDescriptor): void {
ViewletActionItem.draggedViewlet = viewlet;
}
public static clearDraggedViewlet(): void {
ViewletActionItem.draggedViewlet = void 0;
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [ViewletActionItem.toggleViewletPinnedAction];
if (this.viewlet.extensionId) {
actions.push(new Separator());
actions.push(ViewletActionItem.manageExtensionAction);
}
const isPinned = this.activityBarService.isPinned(this.viewlet.id);
if (isPinned) {
ViewletActionItem.toggleViewletPinnedAction.label = nls.localize('removeFromActivityBar', "Remove from Activity Bar");
} else {
ViewletActionItem.toggleViewletPinnedAction.label = nls.localize('keepInActivityBar', "Keep in Activity Bar");
}
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActionsContext: () => this.viewlet,
getActions: () => TPromise.as(actions)
});
}
public focus(): void {
this.$container.domFocus();
}
public set keybinding(keybinding: string) {
this._keybinding = keybinding;
if (!this.$label) {
return;
}
let title: string;
if (keybinding) {
title = nls.localize('titleKeybinding', "{0} ({1})", this.activity.name, keybinding);
} else {
title = this.activity.name;
}
this.$label.title(title);
this.$badge.title(title);
this.$container.title(title);
}
protected _updateClass(): void {
if (this.cssClass) {
this.$badge.removeClass(this.cssClass);
}
this.cssClass = this.getAction().class;
this.$badge.addClass(this.cssClass);
}
protected _updateChecked(): void {
if (this.getAction().checked) {
this.$container.addClass('checked');
} else {
this.$container.removeClass('checked');
}
}
protected _updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
} else {
this.builder.addClass('disabled');
}
}
public dispose(): void {
super.dispose();
ViewletActionItem.clearDraggedViewlet();
this.$label.destroy();
}
}
export class ViewletOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super({
id: 'activitybar.additionalViewlets.action',
name: nls.localize('additionalViews', "Additional Views"),
cssClass: 'toggle-more'
});
}
public run(event): TPromise<any> {
this.showMenu();
return TPromise.as(true);
}
}
export class ViewletOverflowActivityActionItem extends ActivityActionItem {
private name: string;
private cssClass: string;
private actions: OpenViewletAction[];
constructor(
action: ActivityAction,
private getOverflowingViewlets: () => ViewletDescriptor[],
private getBadge: (viewlet: ViewletDescriptor) => IBadge,
@IInstantiationService private instantiationService: IInstantiationService,
@IViewletService private viewletService: IViewletService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
) {
super(action, null, themeService);
this.cssClass = action.class;
this.name = action.label;
}
public showMenu(): void {
if (this.actions) {
dispose(this.actions);
}
this.actions = this.getActions();
this.contextMenuService.showContextMenu({
getAnchor: () => this.builder.getHTMLElement(),
getActions: () => TPromise.as(this.actions),
onHide: () => dispose(this.actions)
});
}
private getActions(): OpenViewletAction[] {
const activeViewlet = this.viewletService.getActiveViewlet();
return this.getOverflowingViewlets().map(viewlet => {
const action = this.instantiationService.createInstance(OpenViewletAction, viewlet);
action.radio = activeViewlet && activeViewlet.getId() === action.id;
const badge = this.getBadge(action.viewlet);
let suffix: string | number;
if (badge instanceof NumberBadge) {
suffix = badge.number;
} else if (badge instanceof TextBadge) {
suffix = badge.text;
}
if (suffix) {
action.label = nls.localize('numberBadge', "{0} ({1})", action.viewlet.name, suffix);
} else {
action.label = action.viewlet.name;
}
return action;
});
}
public dispose(): void {
super.dispose();
this.actions = dispose(this.actions);
}
}
class ManageExtensionAction extends Action {
constructor(
@ICommandService private commandService: ICommandService
) {
super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
public run(viewlet: ViewletDescriptor): TPromise<any> {
return this.commandService.executeCommand('_extensions.manage', viewlet.extensionId);
}
}
class OpenViewletAction extends Action {
export class ToggleViewletAction extends Action {
constructor(
private _viewlet: ViewletDescriptor,
@@ -592,44 +71,16 @@ class OpenViewletAction extends Action {
super(_viewlet.id, _viewlet.name);
}
public get viewlet(): ViewletDescriptor {
return this._viewlet;
}
public run(): TPromise<any> {
const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART);
const activeViewlet = this.viewletService.getActiveViewlet();
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) {
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this._viewlet.id) {
return this.partService.setSideBarHidden(true);
}
return this.viewletService.openViewlet(this.viewlet.id, true);
}
}
export class ToggleViewletPinnedAction extends Action {
constructor(
private viewlet: ViewletDescriptor,
@IActivityBarService private activityBarService: IActivityBarService
) {
super('activitybar.show.toggleViewletPinned', viewlet ? viewlet.name : nls.localize('toggle', "Toggle View Pinned"));
this.checked = this.viewlet && this.activityBarService.isPinned(this.viewlet.id);
}
public run(context?: ViewletDescriptor): TPromise<any> {
const viewlet = this.viewlet || context;
if (this.activityBarService.isPinned(viewlet.id)) {
this.activityBarService.unpin(viewlet.id);
} else {
this.activityBarService.pin(viewlet.id);
}
return TPromise.as(true);
return this.viewletService.openViewlet(this._viewlet.id, true);
}
}
@@ -644,10 +95,11 @@ export class GlobalActivityActionItem extends ActivityActionItem {
constructor(
action: GlobalActivityAction,
colors: ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IContextMenuService protected contextMenuService: IContextMenuService
) {
super(action, { draggable: false }, themeService);
super(action, { draggable: false, colors, icon: true }, themeService);
}
public render(container: HTMLElement): void {
@@ -670,6 +122,13 @@ export class GlobalActivityActionItem extends ActivityActionItem {
this.showContextMenu(this.$container.getHTMLElement());
}
});
this.$container.on(TouchEventType.Tap, (e: GestureEvent) => {
DOM.EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
this.showContextMenu({ x: event.posx, y: event.posy });
});
}
private showContextMenu(location: HTMLElement | { x: number, y: number }): void {

View File

@@ -8,37 +8,40 @@
import 'vs/css!./media/activitybarpart';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import DOM = require('vs/base/browser/dom');
import * as arrays from 'vs/base/common/arrays';
import { illegalArgument } from 'vs/base/common/errors';
import { Builder, $, Dimension } from 'vs/base/browser/builder';
import { Action } from 'vs/base/common/actions';
import { ActionsOrientation, ActionBar, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/browser/activity';
import { ActionsOrientation, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity';
import { Registry } from 'vs/platform/registry/common/platform';
import { Part } from 'vs/workbench/browser/part';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { ToggleViewletPinnedAction, ViewletActivityAction, ActivityAction, GlobalActivityActionItem, ViewletActionItem, ViewletOverflowActivityAction, ViewletOverflowActivityActionItem, GlobalActivityAction, IViewletActivity } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
import { GlobalActivityActionItem, GlobalActivityAction, ViewletActivityAction, ToggleViewletAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IActivityBarService, IBadge } from 'vs/workbench/services/activity/common/activityBarService';
import { IBadge } from 'vs/workbench/services/activity/common/activity';
import { IPartService, Position as SideBarPosition } from 'vs/workbench/services/part/common/partService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Scope as MementoScope } from 'vs/workbench/common/memento';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER } from 'vs/workbench/common/theme';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar';
import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
export class ActivitybarPart extends Part implements IActivityBarService {
export class ActivitybarPart extends Part {
private static readonly ACTIVITY_ACTION_HEIGHT = 50;
private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets';
private static COLORS = {
backgroundColor: ACTIVITY_BAR_FOREGROUND,
badgeBackground: ACTIVITY_BAR_BADGE_BACKGROUND,
badgeForeground: ACTIVITY_BAR_BADGE_FOREGROUND,
dragAndDropBackground: ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND
};
public _serviceBrand: any;
@@ -47,17 +50,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private globalActionBar: ActionBar;
private globalActivityIdToActions: { [globalActivityId: string]: GlobalActivityAction; };
private viewletSwitcherBar: ActionBar;
private viewletOverflowAction: ViewletOverflowActivityAction;
private viewletOverflowActionItem: ViewletOverflowActivityActionItem;
private viewletIdToActions: { [viewletId: string]: ActivityAction; };
private viewletIdToActionItems: { [viewletId: string]: IActionItem; };
private viewletIdToActivityStack: { [viewletId: string]: IViewletActivity[]; };
private memento: object;
private pinnedViewlets: string[];
private activeUnpinnedViewlet: ViewletDescriptor;
private compositeBar: CompositeBar;
constructor(
id: string,
@@ -72,59 +65,54 @@ export class ActivitybarPart extends Part implements IActivityBarService {
super(id, { hasTitle: false }, themeService);
this.globalActivityIdToActions = Object.create(null);
this.compositeBar = this.instantiationService.createInstance(CompositeBar, {
icon: true,
storageId: ActivitybarPart.PINNED_VIEWLETS,
orientation: ActionsOrientation.VERTICAL,
composites: this.getViewlets(), // {{SQL CARBON EDIT}}
openComposite: (compositeId: string) => this.viewletService.openViewlet(compositeId, true),
getActivityAction: (compositeId: string) => this.instantiationService.createInstance(ViewletActivityAction, this.viewletService.getViewlet(compositeId)),
getCompositePinnedAction: (compositeId: string) => new ToggleCompositePinnedAction(this.viewletService.getViewlet(compositeId), this.compositeBar),
getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(ToggleViewletAction, this.viewletService.getViewlet(compositeId)),
getDefaultCompositeId: () => this.viewletService.getDefaultViewletId(),
hidePart: () => this.partService.setSideBarHidden(true),
colors: ActivitybarPart.COLORS,
overflowActionSize: 50
});
this.registerListeners();
}
this.viewletIdToActionItems = Object.create(null);
this.viewletIdToActions = Object.create(null);
this.viewletIdToActivityStack = Object.create(null);
// {{SQL CARBON EDIT}}
private getViewlets(): ViewletDescriptor[] {
const pinnedViewlets = JSON.parse(this.storageService.get(ActivitybarPart.PINNED_VIEWLETS, StorageScope.GLOBAL, null)) as string[];
const allViewLets = this.viewletService.getViewlets();
this.memento = this.getMemento(this.storageService, MementoScope.GLOBAL);
const pinnedViewlets = this.memento[ActivitybarPart.PINNED_VIEWLETS] as string[];
if (pinnedViewlets) {
this.pinnedViewlets = pinnedViewlets
// TODO@Ben: Migrate git => scm viewlet
.map(id => id === 'workbench.view.git' ? 'workbench.view.scm' : id)
.filter(arrays.uniqueFilter<string>(str => str));
} else {
this.pinnedViewlets = this.viewletService.getViewlets().map(v => v.id);
if (!pinnedViewlets) {
return allViewLets.filter(viewlet => viewlet.id !== 'workbench.view.extensions') ;
}
this.registerListeners();
return allViewLets;
}
private registerListeners(): void {
// Activate viewlet action on opening of a viewlet
this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.onDidViewletOpen(viewlet)));
this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId())));
// Deactivate viewlet action on close
this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.onDidViewletClose(viewlet)));
this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.compositeBar.deactivateComposite(viewlet.getId())));
this.toUnbind.push(this.compositeBar.onDidContextMenu(e => this.showContextMenu(e)));
}
private onDidViewletOpen(viewlet: IViewlet): void {
const id = viewlet.getId();
if (this.viewletIdToActions[id]) {
this.viewletIdToActions[id].activate();
public showActivity(viewletOrActionId: string, badge: IBadge, clazz?: string): IDisposable {
if (this.viewletService.getViewlet(viewletOrActionId)) {
return this.compositeBar.showActivity(viewletOrActionId, badge, clazz);
}
const activeUnpinnedViewletShouldClose = this.activeUnpinnedViewlet && this.activeUnpinnedViewlet.id !== viewlet.getId();
const activeUnpinnedViewletShouldShow = !this.getPinnedViewlets().some(v => v.id === viewlet.getId());
if (activeUnpinnedViewletShouldShow || activeUnpinnedViewletShouldClose) {
this.updateViewletSwitcher();
}
return this.showGlobalActivity(viewletOrActionId, badge);
}
private onDidViewletClose(viewlet: IViewlet): void {
const id = viewlet.getId();
if (this.viewletIdToActions[id]) {
this.viewletIdToActions[id].deactivate();
}
}
public showGlobalActivity(globalActivityId: string, badge: IBadge): IDisposable {
private showGlobalActivity(globalActivityId: string, badge: IBadge): IDisposable {
if (!badge) {
throw illegalArgument('badge');
}
@@ -139,87 +127,15 @@ export class ActivitybarPart extends Part implements IActivityBarService {
return toDisposable(() => action.setBadge(undefined));
}
public showActivity(viewletId: string, badge: IBadge, clazz?: string): IDisposable {
if (!badge) {
throw illegalArgument('badge');
}
const activity = <IViewletActivity>{ badge, clazz };
const stack = this.viewletIdToActivityStack[viewletId] || (this.viewletIdToActivityStack[viewletId] = []);
stack.unshift(activity);
this.updateActivity(viewletId);
return {
dispose: () => {
const stack = this.viewletIdToActivityStack[viewletId];
if (!stack) {
return;
}
const idx = stack.indexOf(activity);
if (idx < 0) {
return;
}
stack.splice(idx, 1);
if (stack.length === 0) {
delete this.viewletIdToActivityStack[viewletId];
}
this.updateActivity(viewletId);
}
};
}
private updateActivity(viewletId: string) {
const action = this.viewletIdToActions[viewletId];
if (!action) {
return;
}
const stack = this.viewletIdToActivityStack[viewletId];
if (!stack || !stack.length) {
// reset
action.setBadge(undefined);
} else {
// update
const [{ badge, clazz }] = stack;
action.setBadge(badge);
if (clazz) {
action.class = clazz;
}
}
}
public createContentArea(parent: Builder): Builder {
const $el = $(parent);
const $result = $('.content').appendTo($el);
// Top Actionbar with action items for each viewlet action
this.createViewletSwitcher($result.clone());
this.compositeBar.create($result.getHTMLElement());
// Top Actionbar with action items for each viewlet action
this.createGlobalActivityActionBar($result.getHTMLElement());
// Contextmenu for viewlets
$(parent).on('contextmenu', (e: MouseEvent) => {
DOM.EventHelper.stop(e, true);
this.showContextMenu(e);
}, this.toUnbind);
// Allow to drop at the end to move viewlet to the end
$(parent).on(DOM.EventType.DROP, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
DOM.EventHelper.stop(e, true);
ViewletActionItem.clearDraggedViewlet();
const targetId = this.pinnedViewlets[this.pinnedViewlets.length - 1];
if (targetId !== draggedViewlet.id) {
this.move(draggedViewlet.id, this.pinnedViewlets[this.pinnedViewlets.length - 1]);
}
}
});
this.createGlobalActivityActionBar($('.global-activity').appendTo($result).getHTMLElement());
return $result;
}
@@ -246,31 +162,17 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private showContextMenu(e: MouseEvent): void {
const event = new StandardMouseEvent(e);
const actions: Action[] = this.viewletService.getViewlets().map(viewlet => this.instantiationService.createInstance(ToggleViewletPinnedAction, viewlet));
const actions: Action[] = this.viewletService.getViewlets().map(viewlet => this.instantiationService.createInstance(ToggleCompositePinnedAction, viewlet, this.compositeBar));
actions.push(new Separator());
actions.push(this.instantiationService.createInstance(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, nls.localize('hideActivitBar', "Hide Activity Bar")));
this.contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx + 1, y: event.posy }; },
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => TPromise.as(actions),
onHide: () => dispose(actions)
});
}
private createViewletSwitcher(div: Builder): void {
this.viewletSwitcherBar = new ActionBar(div, {
actionItemProvider: (action: Action) => action instanceof ViewletOverflowActivityAction ? this.viewletOverflowActionItem : this.viewletIdToActionItems[action.id],
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
animated: false
});
this.updateViewletSwitcher();
// Update viewlet switcher when external viewlets become ready
this.extensionService.onReady().then(() => this.updateViewletSwitcher());
}
private createGlobalActivityActionBar(container: HTMLElement): void {
const activityRegistry = Registry.as<IGlobalActivityRegistry>(GlobalActivityExtensions);
const descriptors = activityRegistry.getActivities();
@@ -279,7 +181,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
.map(a => new GlobalActivityAction(a));
this.globalActionBar = new ActionBar(container, {
actionItemProvider: a => this.instantiationService.createInstance(GlobalActivityActionItem, a),
actionItemProvider: a => this.instantiationService.createInstance(GlobalActivityActionItem, a, ActivitybarPart.COLORS),
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('globalActions', "Global Actions"),
animated: false
@@ -291,234 +193,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
});
}
private updateViewletSwitcher() {
if (!this.viewletSwitcherBar) {
return; // We have not been rendered yet so there is nothing to update.
}
let viewletsToShow = this.getPinnedViewlets();
// Always show the active viewlet even if it is marked to be hidden
const activeViewlet = this.viewletService.getActiveViewlet();
if (activeViewlet && !viewletsToShow.some(viewlet => viewlet.id === activeViewlet.getId())) {
this.activeUnpinnedViewlet = this.viewletService.getViewlet(activeViewlet.getId());
viewletsToShow.push(this.activeUnpinnedViewlet);
} else {
this.activeUnpinnedViewlet = void 0;
}
// Ensure we are not showing more viewlets than we have height for
let overflows = false;
if (this.dimension) {
let availableHeight = this.dimension.height;
if (this.globalActionBar) {
availableHeight -= (this.globalActionBar.items.length * ActivitybarPart.ACTIVITY_ACTION_HEIGHT); // adjust for global actions showing
}
const maxVisible = Math.floor(availableHeight / ActivitybarPart.ACTIVITY_ACTION_HEIGHT);
overflows = viewletsToShow.length > maxVisible;
if (overflows) {
viewletsToShow = viewletsToShow.slice(0, maxVisible - 1 /* make room for overflow action */);
}
}
const visibleViewlets = Object.keys(this.viewletIdToActions);
const visibleViewletsChange = !arrays.equals(viewletsToShow.map(viewlet => viewlet.id), visibleViewlets);
// Pull out overflow action if there is a viewlet change so that we can add it to the end later
if (this.viewletOverflowAction && visibleViewletsChange) {
this.viewletSwitcherBar.pull(this.viewletSwitcherBar.length() - 1);
this.viewletOverflowAction.dispose();
this.viewletOverflowAction = null;
this.viewletOverflowActionItem.dispose();
this.viewletOverflowActionItem = null;
}
// Pull out viewlets that overflow or got hidden
const viewletIdsToShow = viewletsToShow.map(v => v.id);
visibleViewlets.forEach(viewletId => {
if (viewletIdsToShow.indexOf(viewletId) === -1) {
this.pullViewlet(viewletId);
}
});
// Built actions for viewlets to show
const newViewletsToShow = viewletsToShow
.filter(viewlet => !this.viewletIdToActions[viewlet.id])
.map(viewlet => this.toAction(viewlet));
// Update when we have new viewlets to show
if (newViewletsToShow.length) {
// Add to viewlet switcher
this.viewletSwitcherBar.push(newViewletsToShow, { label: true, icon: true });
// Make sure to activate the active one
const activeViewlet = this.viewletService.getActiveViewlet();
if (activeViewlet) {
const activeViewletEntry = this.viewletIdToActions[activeViewlet.getId()];
if (activeViewletEntry) {
activeViewletEntry.activate();
}
}
// Make sure to restore activity
Object.keys(this.viewletIdToActions).forEach(viewletId => {
this.updateActivity(viewletId);
});
}
// Add overflow action as needed
if (visibleViewletsChange && overflows) {
this.viewletOverflowAction = this.instantiationService.createInstance(ViewletOverflowActivityAction, () => this.viewletOverflowActionItem.showMenu());
this.viewletOverflowActionItem = this.instantiationService.createInstance(ViewletOverflowActivityActionItem, this.viewletOverflowAction, () => this.getOverflowingViewlets(), (viewlet: ViewletDescriptor) => this.viewletIdToActivityStack[viewlet.id] && this.viewletIdToActivityStack[viewlet.id][0].badge);
this.viewletSwitcherBar.push(this.viewletOverflowAction, { label: true, icon: true });
}
}
private getOverflowingViewlets(): ViewletDescriptor[] {
const viewlets = this.getPinnedViewlets();
if (this.activeUnpinnedViewlet) {
viewlets.push(this.activeUnpinnedViewlet);
}
const visibleViewlets = Object.keys(this.viewletIdToActions);
return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) === -1);
}
private getVisibleViewlets(): ViewletDescriptor[] {
const viewlets = this.viewletService.getViewlets();
const visibleViewlets = Object.keys(this.viewletIdToActions);
return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) >= 0);
}
private getPinnedViewlets(): ViewletDescriptor[] {
return this.pinnedViewlets.map(viewletId => this.viewletService.getViewlet(viewletId)).filter(v => !!v); // ensure to remove those that might no longer exist
}
private pullViewlet(viewletId: string): void {
const index = Object.keys(this.viewletIdToActions).indexOf(viewletId);
if (index >= 0) {
this.viewletSwitcherBar.pull(index);
const action = this.viewletIdToActions[viewletId];
action.dispose();
delete this.viewletIdToActions[viewletId];
const actionItem = this.viewletIdToActionItems[action.id];
actionItem.dispose();
delete this.viewletIdToActionItems[action.id];
}
}
private toAction(viewlet: ViewletDescriptor): ActivityAction {
const action = this.instantiationService.createInstance(ViewletActivityAction, viewlet);
this.viewletIdToActionItems[action.id] = this.instantiationService.createInstance(ViewletActionItem, action);
this.viewletIdToActions[viewlet.id] = action;
return action;
}
public getPinned(): string[] {
return this.pinnedViewlets;
}
public unpin(viewletId: string): void {
if (!this.isPinned(viewletId)) {
return;
}
const activeViewlet = this.viewletService.getActiveViewlet();
const defaultViewletId = this.viewletService.getDefaultViewletId();
const visibleViewlets = this.getVisibleViewlets();
let unpinPromise: TPromise<any>;
// Case: viewlet is not the active one or the active one is a different one
// Solv: we do nothing
if (!activeViewlet || activeViewlet.getId() !== viewletId) {
unpinPromise = TPromise.as(null);
}
// Case: viewlet is not the default viewlet and default viewlet is still showing
// Solv: we open the default viewlet
else if (defaultViewletId !== viewletId && this.isPinned(defaultViewletId)) {
unpinPromise = this.viewletService.openViewlet(defaultViewletId, true);
}
// Case: we closed the last visible viewlet
// Solv: we hide the sidebar
else if (visibleViewlets.length === 1) {
unpinPromise = this.partService.setSideBarHidden(true);
}
// Case: we closed the default viewlet
// Solv: we open the next visible viewlet from top
else {
unpinPromise = this.viewletService.openViewlet(visibleViewlets.filter(viewlet => viewlet.id !== viewletId)[0].id, true);
}
unpinPromise.then(() => {
// then remove from pinned and update switcher
const index = this.pinnedViewlets.indexOf(viewletId);
this.pinnedViewlets.splice(index, 1);
this.updateViewletSwitcher();
});
}
public isPinned(viewletId: string): boolean {
return this.pinnedViewlets.indexOf(viewletId) >= 0;
}
public pin(viewletId: string, update = true): void {
if (this.isPinned(viewletId)) {
return;
}
// first open that viewlet
this.viewletService.openViewlet(viewletId, true).then(() => {
// then update
this.pinnedViewlets.push(viewletId);
this.pinnedViewlets = arrays.distinct(this.pinnedViewlets);
if (update) {
this.updateViewletSwitcher();
}
});
}
public move(viewletId: string, toViewletId: string): void {
// Make sure a moved viewlet gets pinned
if (!this.isPinned(viewletId)) {
this.pin(viewletId, false /* defer update, we take care of it */);
}
const fromIndex = this.pinnedViewlets.indexOf(viewletId);
const toIndex = this.pinnedViewlets.indexOf(toViewletId);
this.pinnedViewlets.splice(fromIndex, 1);
this.pinnedViewlets.splice(toIndex, 0, viewletId);
// Clear viewlets that are impacted by the move
const visibleViewlets = Object.keys(this.viewletIdToActions);
for (let i = Math.min(fromIndex, toIndex); i < visibleViewlets.length; i++) {
this.pullViewlet(visibleViewlets[i]);
}
// timeout helps to prevent artifacts from showing up
setTimeout(() => {
this.updateViewletSwitcher();
}, 0);
return this.viewletService.getViewlets().map(v => v.id).filter(id => this.compositeBar.isPinned(id));;
}
/**
@@ -531,16 +207,20 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.dimension = sizes[1];
// Update switcher to handle overflow issues
this.updateViewletSwitcher();
let availableHeight = this.dimension.height;
if (this.globalActionBar) {
// adjust height for global actions showing
availableHeight -= (this.globalActionBar.items.length * this.globalActionBar.domNode.clientHeight);
}
this.compositeBar.layout(new Dimension(dimension.width, availableHeight));
return sizes;
}
public dispose(): void {
if (this.viewletSwitcherBar) {
this.viewletSwitcherBar.dispose();
this.viewletSwitcherBar = null;
if (this.compositeBar) {
this.compositeBar.dispose();
this.compositeBar = null;
}
if (this.globalActionBar) {
@@ -552,11 +232,10 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}
public shutdown(): void {
// Persist Hidden State
this.memento[ActivitybarPart.PINNED_VIEWLETS] = this.pinnedViewlets;
this.compositeBar.store();
// Pass to super
super.shutdown();
}
}
}

View File

@@ -41,10 +41,6 @@
right: 1px;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.toggle-more {
-webkit-mask: url('ellipsis-global.svg') no-repeat 50% 50%;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .badge {
position: absolute;
top: 5px;

View File

@@ -21,4 +21,8 @@
.monaco-workbench > .activitybar [tabindex="0"]:focus {
outline: 0 !important; /* activity bar indicates focus custom */
}
.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-label.toggle-more {
-webkit-mask: url('ellipsis-global.svg') no-repeat 50% 50%;
}

View File

@@ -62,8 +62,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
private lastActiveCompositeId: string;
private instantiatedComposites: Composite[];
private titleLabel: ICompositeTitleLabel;
private toolBar: ToolBar;
private compositeLoaderPromises: { [compositeId: string]: TPromise<Composite>; };
protected toolBar: ToolBar;
private progressBar: ProgressBar;
private contentAreaSize: Dimension;
private telemetryActionsListener: IDisposable;
@@ -75,7 +74,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
private messageService: IMessageService,
private storageService: IStorageService,
private telemetryService: ITelemetryService,
private contextMenuService: IContextMenuService,
protected contextMenuService: IContextMenuService,
protected partService: IPartService,
private keybindingService: IKeybindingService,
protected instantiationService: IInstantiationService,
@@ -98,7 +97,6 @@ export abstract class CompositePart<T extends Composite> extends Part {
this.mapProgressServiceToComposite = {};
this.activeComposite = null;
this.instantiatedComposites = [];
this.compositeLoaderPromises = {};
this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
}
@@ -120,7 +118,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
private doOpenComposite(id: string, focus?: boolean): TPromise<Composite> {
// Use a generated token to avoid race conditions from long running promises
let currentCompositeOpenToken = defaultGenerator.nextId();
const currentCompositeOpenToken = defaultGenerator.nextId();
this.currentCompositeOpenToken = currentCompositeOpenToken;
// Hide current
@@ -137,32 +135,31 @@ export abstract class CompositePart<T extends Composite> extends Part {
this.updateTitle(id);
// Create composite
return this.createComposite(id, true).then(composite => {
const composite = this.createComposite(id, true);
// Check if another composite opened meanwhile and return in that case
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
return TPromise.as(null);
// Check if another composite opened meanwhile and return in that case
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
return TPromise.as(null);
}
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
if (focus) {
composite.focus();
}
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
if (focus) {
composite.focus();
}
// Fullfill promise with composite that is being opened
return TPromise.as(composite);
}
// Fullfill promise with composite that is being opened
return TPromise.as(composite);
// Show Composite and Focus
return this.showComposite(composite).then(() => {
if (focus) {
composite.focus();
}
// Show Composite and Focus
return this.showComposite(composite).then(() => {
if (focus) {
composite.focus();
}
// Fullfill promise with composite that is being opened
return composite;
});
// Fullfill promise with composite that is being opened
return composite;
});
}).then(composite => {
if (composite) {
@@ -173,46 +170,31 @@ export abstract class CompositePart<T extends Composite> extends Part {
});
}
protected createComposite(id: string, isActive?: boolean): TPromise<Composite> {
protected createComposite(id: string, isActive?: boolean): Composite {
// Check if composite is already created
for (let i = 0; i < this.instantiatedComposites.length; i++) {
if (this.instantiatedComposites[i].getId() === id) {
return TPromise.as(this.instantiatedComposites[i]);
return this.instantiatedComposites[i];
}
}
// Instantiate composite from registry otherwise
let compositeDescriptor = this.registry.getComposite(id);
const compositeDescriptor = this.registry.getComposite(id);
if (compositeDescriptor) {
let loaderPromise = this.compositeLoaderPromises[id];
if (!loaderPromise) {
let progressService = this.instantiationService.createInstance(WorkbenchProgressService, this.progressBar, compositeDescriptor.id, isActive);
let compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));
const progressService = this.instantiationService.createInstance(WorkbenchProgressService, this.progressBar, compositeDescriptor.id, isActive);
const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));
loaderPromise = compositeInstantiationService.createInstance(compositeDescriptor).then((composite: Composite) => {
this.mapProgressServiceToComposite[composite.getId()] = progressService;
const composite = compositeDescriptor.instantiate(compositeInstantiationService);
this.mapProgressServiceToComposite[composite.getId()] = progressService;
// Remember as Instantiated
this.instantiatedComposites.push(composite);
// Remember as Instantiated
this.instantiatedComposites.push(composite);
// Register to title area update events from the composite
this.instantiatedCompositeListeners.push(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId())));
// Register to title area update events from the composite
this.instantiatedCompositeListeners.push(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId())));
// Remove from Promises Cache since Loaded
delete this.compositeLoaderPromises[id];
return composite;
});
// Report progress for slow loading composites
progressService.showWhile(loaderPromise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
// Add to Promise Cache until Loaded
this.compositeLoaderPromises[id] = loaderPromise;
}
return loaderPromise;
return composite;
}
throw new Error(strings.format('Unable to find composite with id {0}', id));
@@ -260,7 +242,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
}
// Report progress for slow loading composites (but only if we did not create the composites before already)
let progressService = this.mapProgressServiceToComposite[composite.getId()];
const progressService = this.mapProgressServiceToComposite[composite.getId()];
if (progressService && !compositeContainer) {
this.mapProgressServiceToComposite[composite.getId()].showWhile(createCompositePromise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
}
@@ -281,7 +263,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
this.toolBar.actionRunner = composite.getActionRunner();
// Update title with composite title if it differs from descriptor
let descriptor = this.registry.getComposite(composite.getId());
const descriptor = this.registry.getComposite(composite.getId());
if (descriptor && descriptor.name !== composite.getTitle()) {
this.updateTitle(composite.getId(), composite.getTitle());
}
@@ -309,6 +291,12 @@ export abstract class CompositePart<T extends Composite> extends Part {
// Log in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
}
});
@@ -338,7 +326,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
this.updateTitle(this.activeComposite.getId(), this.activeComposite.getTitle());
// Actions
let actionsBinding = this.collectCompositeActions(this.activeComposite);
const actionsBinding = this.collectCompositeActions(this.activeComposite);
this.mapActionsBindingToComposite[this.activeComposite.getId()] = actionsBinding;
actionsBinding();
}
@@ -350,8 +338,8 @@ export abstract class CompositePart<T extends Composite> extends Part {
}
private updateTitle(compositeId: string, compositeTitle?: string): void {
let compositeDescriptor = this.registry.getComposite(compositeId);
if (!compositeDescriptor) {
const compositeDescriptor = this.registry.getComposite(compositeId);
if (!compositeDescriptor || !this.titleLabel) {
return;
}
@@ -369,15 +357,15 @@ export abstract class CompositePart<T extends Composite> extends Part {
private collectCompositeActions(composite: Composite): () => void {
// From Composite
let primaryActions: IAction[] = composite.getActions().slice(0);
let secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);
const primaryActions: IAction[] = composite.getActions().slice(0);
const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);
// From Part
primaryActions.push(...this.getActions());
secondaryActions.push(...this.getSecondaryActions());
// From Contributions
let actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
const actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
primaryActions.push(...actionBarRegistry.getActionBarActionsForContext(this.actionContributionScope, composite));
secondaryActions.push(...actionBarRegistry.getSecondaryActionBarActionsForContext(this.actionContributionScope, composite));
@@ -398,10 +386,10 @@ export abstract class CompositePart<T extends Composite> extends Part {
return TPromise.as(null); // Nothing to do
}
let composite = this.activeComposite;
const composite = this.activeComposite;
this.activeComposite = null;
let compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
// Indicate to Composite
return composite.setVisible(false).then(() => {
@@ -424,7 +412,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
public createTitleArea(parent: Builder): Builder {
// Title Area Container
let titleArea = $(parent).div({
const titleArea = $(parent).div({
'class': ['composite', 'title']
});
@@ -480,7 +468,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
if (this.activeComposite) {
const contextMenuActions = this.getTitleAreaContextMenuActions();
if (contextMenuActions.length) {
let anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
const anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(contextMenuActions),
@@ -506,7 +494,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
// Check Registry
if (!actionItem) {
let actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
const actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
actionItem = actionBarRegistry.getActionItemForContext(this.actionContributionScope, ToolBarContext, action);
}
@@ -542,7 +530,7 @@ export abstract class CompositePart<T extends Composite> extends Part {
public layout(dimension: Dimension): Dimension[] {
// Pass to super
let sizes = super.layout(dimension);
const sizes = super.layout(dimension);
// Pass Contentsize to composite
this.contentAreaSize = sizes[1];

View File

@@ -0,0 +1,459 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { illegalArgument } from 'vs/base/common/errors';
import * as dom from 'vs/base/browser/dom';
import * as arrays from 'vs/base/common/arrays';
import { Dimension } from 'vs/base/browser/builder';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IBadge } from 'vs/workbench/services/activity/common/activity';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ActionBar, IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import Event, { Emitter } from 'vs/base/common/event';
import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { TPromise } from 'vs/base/common/winjs.base';
export interface ICompositeBarOptions {
icon: boolean;
storageId: string;
orientation: ActionsOrientation;
composites: { id: string, name: string }[];
colors: ICompositeBarColors;
overflowActionSize: number;
getActivityAction: (compositeId: string) => ActivityAction;
getCompositePinnedAction: (compositeId: string) => Action;
getOnCompositeClickAction: (compositeId: string) => Action;
openComposite: (compositeId: string) => TPromise<any>;
getDefaultCompositeId: () => string;
hidePart: () => TPromise<any>;
}
export class CompositeBar implements ICompositeBar {
private _onDidContextMenu: Emitter<MouseEvent>;
private dimension: Dimension;
private toDispose: IDisposable[];
private compositeSwitcherBar: ActionBar;
private compositeOverflowAction: CompositeOverflowActivityAction;
private compositeOverflowActionItem: CompositeOverflowActivityActionItem;
private compositeIdToActions: { [compositeId: string]: ActivityAction; };
private compositeIdToActionItems: { [compositeId: string]: IActionItem; };
private compositeIdToActivityStack: { [compositeId: string]: ICompositeActivity[]; };
private compositeSizeInBar: Map<string, number>;
private pinnedComposites: string[];
private activeCompositeId: string;
private activeUnpinnedCompositeId: string;
constructor(
private options: ICompositeBarOptions,
@IInstantiationService private instantiationService: IInstantiationService,
@IStorageService private storageService: IStorageService,
) {
this.toDispose = [];
this.compositeIdToActionItems = Object.create(null);
this.compositeIdToActions = Object.create(null);
this.compositeIdToActivityStack = Object.create(null);
this.compositeSizeInBar = new Map<string, number>();
this._onDidContextMenu = new Emitter<MouseEvent>();
const pinnedComposites = JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, null)) as string[];
if (pinnedComposites) {
this.pinnedComposites = pinnedComposites;
} else {
this.pinnedComposites = this.options.composites.map(c => c.id);
}
}
public get onDidContextMenu(): Event<MouseEvent> {
return this._onDidContextMenu.event;
}
public activateComposite(id: string): void {
if (this.compositeIdToActions[id]) {
this.compositeIdToActions[id].activate();
}
this.activeCompositeId = id;
const activeUnpinnedCompositeShouldClose = this.activeUnpinnedCompositeId && this.activeUnpinnedCompositeId !== id;
const activeUnpinnedCompositeShouldShow = !this.pinnedComposites.some(pid => pid === id);
if (activeUnpinnedCompositeShouldShow || activeUnpinnedCompositeShouldClose) {
this.updateCompositeSwitcher();
}
}
public deactivateComposite(id: string): void {
if (this.compositeIdToActions[id]) {
this.compositeIdToActions[id].deactivate();
}
}
public showActivity(compositeId: string, badge: IBadge, clazz?: string): IDisposable {
if (!badge) {
throw illegalArgument('badge');
}
const activity = <ICompositeActivity>{ badge, clazz };
const stack = this.compositeIdToActivityStack[compositeId] || (this.compositeIdToActivityStack[compositeId] = []);
stack.unshift(activity);
this.updateActivity(compositeId);
return {
dispose: () => {
const stack = this.compositeIdToActivityStack[compositeId];
if (!stack) {
return;
}
const idx = stack.indexOf(activity);
if (idx < 0) {
return;
}
stack.splice(idx, 1);
if (stack.length === 0) {
delete this.compositeIdToActivityStack[compositeId];
}
this.updateActivity(compositeId);
}
};
}
private updateActivity(compositeId: string) {
const action = this.compositeIdToActions[compositeId];
if (!action) {
return;
}
const stack = this.compositeIdToActivityStack[compositeId];
// reset
if (!stack || !stack.length) {
action.setBadge(undefined);
}
// update
else {
const [{ badge, clazz }] = stack;
action.setBadge(badge);
if (clazz) {
action.class = clazz;
}
}
}
public create(parent: HTMLElement): HTMLElement {
const actionBarDiv = parent.appendChild(dom.$('.composite-bar'));
this.compositeSwitcherBar = new ActionBar(actionBarDiv, {
actionItemProvider: (action: Action) => action instanceof CompositeOverflowActivityAction ? this.compositeOverflowActionItem : this.compositeIdToActionItems[action.id],
orientation: this.options.orientation,
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
animated: false,
});
// Contextmenu for composites
this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => {
dom.EventHelper.stop(e, true);
this._onDidContextMenu.fire(e);
}));
// Allow to drop at the end to move composites to the end
this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.DROP, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
dom.EventHelper.stop(e, true);
CompositeActionItem.clearDraggedComposite();
const targetId = this.pinnedComposites[this.pinnedComposites.length - 1];
if (targetId !== draggedCompositeId) {
this.move(draggedCompositeId, this.pinnedComposites[this.pinnedComposites.length - 1]);
}
}
}));
return actionBarDiv;
}
public getAction(compositeId): ActivityAction {
return this.compositeIdToActions[compositeId];
}
private updateCompositeSwitcher(): void {
if (!this.compositeSwitcherBar || !this.dimension) {
return; // We have not been rendered yet so there is nothing to update.
}
let compositesToShow = this.pinnedComposites;
// Always show the active composite even if it is marked to be hidden
if (this.activeCompositeId && !compositesToShow.some(id => id === this.activeCompositeId)) {
this.activeUnpinnedCompositeId = this.activeCompositeId;
compositesToShow = compositesToShow.concat(this.activeUnpinnedCompositeId);
} else {
this.activeUnpinnedCompositeId = void 0;
}
// Ensure we are not showing more composites than we have height for
let overflows = false;
let maxVisible = compositesToShow.length;
let size = 0;
const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width;
for (let i = 0; i < compositesToShow.length && size <= limit; i++) {
size += this.compositeSizeInBar.get(compositesToShow[i]);
if (size > limit) {
maxVisible = i;
}
}
overflows = compositesToShow.length > maxVisible;
if (overflows) {
size -= this.compositeSizeInBar.get(compositesToShow[maxVisible]);
compositesToShow = compositesToShow.slice(0, maxVisible);
}
// Check if we need to make extra room for the overflow action
if (overflows && (size + this.options.overflowActionSize > limit)) {
compositesToShow.pop();
}
if (this.activeCompositeId && compositesToShow.length && compositesToShow.indexOf(this.activeCompositeId) === -1) {
compositesToShow.pop();
compositesToShow.push(this.activeCompositeId);
}
const visibleComposites = Object.keys(this.compositeIdToActions);
const visibleCompositesChange = !arrays.equals(compositesToShow, visibleComposites);
// Pull out overflow action if there is a composite change so that we can add it to the end later
if (this.compositeOverflowAction && visibleCompositesChange) {
this.compositeSwitcherBar.pull(this.compositeSwitcherBar.length() - 1);
this.compositeOverflowAction.dispose();
this.compositeOverflowAction = null;
this.compositeOverflowActionItem.dispose();
this.compositeOverflowActionItem = null;
}
// Pull out composites that overflow, got hidden or changed position
visibleComposites.forEach((compositeId, index) => {
if (compositesToShow.indexOf(compositeId) !== index) {
this.pullComposite(compositeId);
}
});
// Built actions for composites to show
const newCompositesToShow = compositesToShow
.filter(compositeId => !this.compositeIdToActions[compositeId])
.map(compositeId => this.toAction(compositeId));
// Update when we have new composites to show
if (newCompositesToShow.length) {
// Add to composite switcher
this.compositeSwitcherBar.push(newCompositesToShow, { label: true, icon: this.options.icon });
// Make sure to activate the active one
if (this.activeCompositeId) {
const activeCompositeEntry = this.compositeIdToActions[this.activeCompositeId];
if (activeCompositeEntry) {
activeCompositeEntry.activate();
}
}
// Make sure to restore activity
Object.keys(this.compositeIdToActions).forEach(compositeId => {
this.updateActivity(compositeId);
});
}
// Add overflow action as needed
if ((visibleCompositesChange && overflows) || this.compositeSwitcherBar.length() === 0) {
this.compositeOverflowAction = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => this.compositeOverflowActionItem.showMenu());
this.compositeOverflowActionItem = this.instantiationService.createInstance(
CompositeOverflowActivityActionItem,
this.compositeOverflowAction,
() => this.getOverflowingComposites(),
() => this.activeCompositeId,
(compositeId: string) => this.compositeIdToActivityStack[compositeId] && this.compositeIdToActivityStack[compositeId][0].badge,
this.options.getOnCompositeClickAction,
this.options.colors
);
this.compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true });
}
}
private getOverflowingComposites(): { id: string, name: string }[] {
let overflowingIds = this.pinnedComposites;
if (this.activeUnpinnedCompositeId) {
overflowingIds = overflowingIds.concat(this.activeUnpinnedCompositeId);
}
const visibleComposites = Object.keys(this.compositeIdToActions);
overflowingIds = overflowingIds.filter(compositeId => visibleComposites.indexOf(compositeId) === -1);
return this.options.composites.filter(c => overflowingIds.indexOf(c.id) !== -1);
}
private getVisibleComposites(): string[] {
return Object.keys(this.compositeIdToActions);
}
private pullComposite(compositeId: string): void {
const index = Object.keys(this.compositeIdToActions).indexOf(compositeId);
if (index >= 0) {
this.compositeSwitcherBar.pull(index);
const action = this.compositeIdToActions[compositeId];
action.dispose();
delete this.compositeIdToActions[compositeId];
const actionItem = this.compositeIdToActionItems[action.id];
actionItem.dispose();
delete this.compositeIdToActionItems[action.id];
}
}
private toAction(compositeId: string): ActivityAction {
if (this.compositeIdToActions[compositeId]) {
return this.compositeIdToActions[compositeId];
}
const compositeActivityAction = this.options.getActivityAction(compositeId);
const pinnedAction = this.options.getCompositePinnedAction(compositeId);
this.compositeIdToActionItems[compositeId] = this.instantiationService.createInstance(CompositeActionItem, compositeActivityAction, pinnedAction, this.options.colors, this.options.icon, this);
this.compositeIdToActions[compositeId] = compositeActivityAction;
return compositeActivityAction;
}
public unpin(compositeId: string): void {
if (!this.isPinned(compositeId)) {
return;
}
const defaultCompositeId = this.options.getDefaultCompositeId();
const visibleComposites = this.getVisibleComposites();
let unpinPromise: TPromise<any>;
// Case: composite is not the active one or the active one is a different one
// Solv: we do nothing
if (!this.activeCompositeId || this.activeCompositeId !== compositeId) {
unpinPromise = TPromise.as(null);
}
// Case: composite is not the default composite and default composite is still showing
// Solv: we open the default composite
else if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) {
unpinPromise = this.options.openComposite(defaultCompositeId);
}
// Case: we closed the last visible composite
// Solv: we hide the part
else if (visibleComposites.length === 1) {
unpinPromise = this.options.hidePart();
}
// Case: we closed the default composite
// Solv: we open the next visible composite from top
else {
unpinPromise = this.options.openComposite(visibleComposites.filter(cid => cid !== compositeId)[0]);
}
unpinPromise.then(() => {
// then remove from pinned and update switcher
const index = this.pinnedComposites.indexOf(compositeId);
this.pinnedComposites.splice(index, 1);
this.updateCompositeSwitcher();
});
}
public isPinned(compositeId: string): boolean {
return this.pinnedComposites.indexOf(compositeId) >= 0;
}
public pin(compositeId: string, update = true): void {
if (this.isPinned(compositeId)) {
return;
}
this.options.openComposite(compositeId).then(() => {
this.pinnedComposites.push(compositeId);
this.pinnedComposites = arrays.distinct(this.pinnedComposites);
if (update) {
this.updateCompositeSwitcher();
}
});
}
public move(compositeId: string, toCompositeId: string): void {
// Make sure both composites are known to this composite bar
if (this.options.composites.filter(c => c.id === compositeId || c.id === toCompositeId).length !== 2) {
return;
}
// Make sure a moved composite gets pinned
if (!this.isPinned(compositeId)) {
this.pin(compositeId, false /* defer update, we take care of it */);
}
const fromIndex = this.pinnedComposites.indexOf(compositeId);
const toIndex = this.pinnedComposites.indexOf(toCompositeId);
this.pinnedComposites.splice(fromIndex, 1);
this.pinnedComposites.splice(toIndex, 0, compositeId);
// Clear composites that are impacted by the move
const visibleComposites = Object.keys(this.compositeIdToActions);
for (let i = Math.min(fromIndex, toIndex); i < visibleComposites.length; i++) {
this.pullComposite(visibleComposites[i]);
}
// timeout helps to prevent artifacts from showing up
setTimeout(() => {
this.updateCompositeSwitcher();
}, 0);
}
public layout(dimension: Dimension): void {
this.dimension = dimension;
if (dimension.height === 0 || dimension.width === 0) {
// Do not layout if not visible. Otherwise the size measurment would be computed wrongly
return;
}
if (this.compositeSizeInBar.size === 0) {
// Compute size of each composite by getting the size from the css renderer
// Size is later used for overflow computation
this.compositeSwitcherBar.clear();
this.compositeSwitcherBar.push(this.options.composites.map(c => this.options.getActivityAction(c.id)));
this.options.composites.map((c, index) => this.compositeSizeInBar.set(c.id, this.options.orientation === ActionsOrientation.VERTICAL
? this.compositeSwitcherBar.getHeight(index)
: this.compositeSwitcherBar.getWidth(index)
));
this.compositeSwitcherBar.clear();
}
this.updateCompositeSwitcher();
}
public store(): void {
this.storageService.store(this.options.storageId, JSON.stringify(this.pinnedComposites), StorageScope.GLOBAL);
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,605 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import * as dom from 'vs/base/browser/dom';
import { Builder, $ } from 'vs/base/browser/builder';
import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { dispose } from 'vs/base/common/lifecycle';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IActivity } from 'vs/workbench/common/activity';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import Event, { Emitter } from 'vs/base/common/event';
export interface ICompositeActivity {
badge: IBadge;
clazz: string;
}
export interface ICompositeBar {
/**
* Unpins a composite from the composite bar.
*/
unpin(compositeId: string): void;
/**
* Pin a composite inside the composite bar.
*/
pin(compositeId: string): void;
/**
* Find out if a composite is pinned in the composite bar.
*/
isPinned(compositeId: string): boolean;
/**
* Reorder composite ordering by moving a composite to the location of another composite.
*/
move(compositeId: string, tocompositeId: string): void;
}
export class ActivityAction extends Action {
private badge: IBadge;
private _onDidChangeBadge = new Emitter<this>();
constructor(private _activity: IActivity) {
super(_activity.id, _activity.name, _activity.cssClass);
this.badge = null;
}
public get activity(): IActivity {
return this._activity;
}
public get onDidChangeBadge(): Event<this> {
return this._onDidChangeBadge.event;
}
public activate(): void {
if (!this.checked) {
this._setChecked(true);
}
}
public deactivate(): void {
if (this.checked) {
this._setChecked(false);
}
}
public getBadge(): IBadge {
return this.badge;
}
public setBadge(badge: IBadge): void {
this.badge = badge;
this._onDidChangeBadge.fire(this);
}
}
export interface ICompositeBarColors {
backgroundColor: string;
badgeBackground: string;
badgeForeground: string;
dragAndDropBackground: string;
}
export interface IActivityActionItemOptions extends IBaseActionItemOptions {
icon?: boolean;
colors: ICompositeBarColors;
}
export class ActivityActionItem extends BaseActionItem {
protected $container: Builder;
protected $label: Builder;
protected $badge: Builder;
protected options: IActivityActionItemOptions;
private $badgeContent: Builder;
private mouseUpTimeout: number;
constructor(
action: ActivityAction,
options: IActivityActionItemOptions,
@IThemeService protected themeService: IThemeService
) {
super(null, action, options);
this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose);
action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose);
}
protected get activity(): IActivity {
return (this._action as ActivityAction).activity;
}
protected updateStyles(): void {
const theme = this.themeService.getTheme();
// Label
if (this.$label && this.options.icon) {
const background = theme.getColor(this.options.colors.backgroundColor);
this.$label.style('background-color', background ? background.toString() : null);
}
// Badge
if (this.$badgeContent) {
const badgeForeground = theme.getColor(this.options.colors.badgeForeground);
const badgeBackground = theme.getColor(this.options.colors.badgeBackground);
const contrastBorderColor = theme.getColor(contrastBorder);
this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null);
this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null);
this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null);
this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null);
this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null);
}
}
public render(container: HTMLElement): void {
super.render(container);
// Make the container tab-able for keyboard navigation
this.$container = $(container).attr({
tabIndex: '0',
role: 'button',
title: this.activity.name
});
// Try hard to prevent keyboard only focus feedback when using mouse
this.$container.on(dom.EventType.MOUSE_DOWN, () => {
this.$container.addClass('clicked');
});
this.$container.on(dom.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$container.removeClass('clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
// Label
this.$label = $('a.action-label').appendTo(this.builder);
if (this.activity.cssClass) {
this.$label.addClass(this.activity.cssClass);
}
if (!this.options.icon) {
this.$label.text(this.getAction().label);
}
this.$badge = this.builder.clone().div({ 'class': 'badge' }, (badge: Builder) => {
this.$badgeContent = badge.div({ 'class': 'badge-content' });
});
this.$badge.hide();
this.updateStyles();
}
private onThemeChange(theme: ITheme): void {
this.updateStyles();
}
public setBadge(badge: IBadge): void {
this.updateBadge(badge);
}
protected updateBadge(badge: IBadge): void {
this.$badgeContent.empty();
this.$badge.hide();
if (badge) {
// Number
if (badge instanceof NumberBadge) {
if (badge.number) {
this.$badgeContent.text(badge.number > 99 ? '99+' : badge.number.toString());
this.$badge.show();
}
}
// Text
else if (badge instanceof TextBadge) {
this.$badgeContent.text(badge.text);
this.$badge.show();
}
// Text
else if (badge instanceof IconBadge) {
this.$badge.show();
}
// Progress
else if (badge instanceof ProgressBadge) {
this.$badge.show();
}
}
// Title
let title: string;
if (badge && badge.getDescription()) {
if (this.activity.name) {
title = nls.localize('badgeTitle', "{0} - {1}", this.activity.name, badge.getDescription());
} else {
title = badge.getDescription();
}
} else {
title = this.activity.name;
}
[this.$label, this.$badge, this.$container].forEach(b => {
if (b) {
b.attr('aria-label', title);
b.title(title);
}
});
}
private handleBadgeChangeEvenet(): void {
const action = this.getAction();
if (action instanceof ActivityAction) {
this.updateBadge(action.getBadge());
}
}
public dispose(): void {
super.dispose();
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
}
}
export class CompositeOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super({
id: 'additionalComposites.action',
name: nls.localize('additionalViews', "Additional Views"),
cssClass: 'toggle-more'
});
}
public run(event: any): TPromise<any> {
this.showMenu();
return TPromise.as(true);
}
}
export class CompositeOverflowActivityActionItem extends ActivityActionItem {
private name: string;
private cssClass: string;
private actions: Action[];
constructor(
action: ActivityAction,
private getOverflowingComposites: () => { id: string, name: string }[],
private getActiveCompositeId: () => string,
private getBadge: (compositeId: string) => IBadge,
private getCompositeOpenAction: (compositeId: string) => Action,
colors: ICompositeBarColors,
@IInstantiationService private instantiationService: IInstantiationService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
) {
super(action, { icon: true, colors }, themeService);
this.cssClass = action.class;
this.name = action.label;
}
public showMenu(): void {
if (this.actions) {
dispose(this.actions);
}
this.actions = this.getActions();
this.contextMenuService.showContextMenu({
getAnchor: () => this.builder.getHTMLElement(),
getActions: () => TPromise.as(this.actions),
onHide: () => dispose(this.actions)
});
}
private getActions(): Action[] {
return this.getOverflowingComposites().map(composite => {
const action = this.getCompositeOpenAction(composite.id);
action.radio = this.getActiveCompositeId() === action.id;
const badge = this.getBadge(composite.id);
let suffix: string | number;
if (badge instanceof NumberBadge) {
suffix = badge.number;
} else if (badge instanceof TextBadge) {
suffix = badge.text;
}
if (suffix) {
action.label = nls.localize('numberBadge', "{0} ({1})", composite.name, suffix);
} else {
action.label = composite.name;
}
return action;
});
}
public dispose(): void {
super.dispose();
this.actions = dispose(this.actions);
}
}
class ManageExtensionAction extends Action {
constructor(
@ICommandService private commandService: ICommandService
) {
super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
public run(id: string): TPromise<any> {
return this.commandService.executeCommand('_extensions.manage', id);
}
}
export class CompositeActionItem extends ActivityActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static draggedCompositeId: string;
private compositeActivity: IActivity;
private cssClass: string;
constructor(
private compositeActivityAction: ActivityAction,
private toggleCompositePinnedAction: Action,
colors: ICompositeBarColors,
icon: boolean,
private compositeBar: ICompositeBar,
@IContextMenuService private contextMenuService: IContextMenuService,
@IKeybindingService private keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super(compositeActivityAction, { draggable: true, colors, icon }, themeService);
this.cssClass = compositeActivityAction.class;
if (!CompositeActionItem.manageExtensionAction) {
CompositeActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction);
}
}
protected get activity(): IActivity {
if (!this.compositeActivity) {
let activityName: string;
const keybinding = this.getKeybindingLabel(this.compositeActivityAction.activity.keybindingId);
if (keybinding) {
activityName = nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding);
} else {
activityName = this.compositeActivityAction.activity.name;
}
this.compositeActivity = {
id: this.compositeActivityAction.activity.id,
cssClass: this.cssClass,
name: activityName
};
}
return this.compositeActivity;
}
private getKeybindingLabel(id: string): string {
const kb = this.keybindingService.lookupKeybinding(id);
if (kb) {
return kb.getLabel();
}
return null;
}
public render(container: HTMLElement): void {
super.render(container);
this.$container.on('contextmenu', e => {
dom.EventHelper.stop(e, true);
this.showContextMenu(container);
});
// Allow to drag
this.$container.on(dom.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
this.setDraggedComposite(this.activity.id);
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
});
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
this.$container.on(dom.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
counter++;
this.updateFromDragging(container, true);
}
});
// Drag leave
this.$container.on(dom.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter--;
if (counter === 0) {
this.updateFromDragging(container, false);
}
}
});
// Drag end
this.$container.on(dom.EventType.DRAG_END, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter = 0;
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
}
});
// Drop
this.$container.on(dom.EventType.DROP, (e: DragEvent) => {
dom.EventHelper.stop(e, true);
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
this.compositeBar.move(draggedCompositeId, this.activity.id);
}
});
// Activate on drag over to reveal targets
[this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!CompositeActionItem.getDraggedCompositeId() && !this.getAction().checked) {
this.getAction().run();
}
}));
this.updateStyles();
}
private updateFromDragging(element: HTMLElement, isDragging: boolean): void {
const theme = this.themeService.getTheme();
const dragBackground = theme.getColor(this.options.colors.dragAndDropBackground);
element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null;
}
public static getDraggedCompositeId(): string {
return CompositeActionItem.draggedCompositeId;
}
private setDraggedComposite(compositeId: string): void {
CompositeActionItem.draggedCompositeId = compositeId;
}
public static clearDraggedComposite(): void {
CompositeActionItem.draggedCompositeId = void 0;
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [this.toggleCompositePinnedAction];
if ((<any>this.compositeActivityAction.activity).extensionId) {
actions.push(new Separator());
actions.push(CompositeActionItem.manageExtensionAction);
}
const isPinned = this.compositeBar.isPinned(this.activity.id);
if (isPinned) {
this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide");
this.toggleCompositePinnedAction.checked = false;
} else {
this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep");
}
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActionsContext: () => this.activity.id,
getActions: () => TPromise.as(actions)
});
}
public focus(): void {
this.$container.domFocus();
}
protected _updateClass(): void {
if (this.cssClass) {
this.$badge.removeClass(this.cssClass);
}
this.cssClass = this.getAction().class;
this.$badge.addClass(this.cssClass);
}
protected _updateChecked(): void {
if (this.getAction().checked) {
this.$container.addClass('checked');
} else {
this.$container.removeClass('checked');
}
}
protected _updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
} else {
this.builder.addClass('disabled');
}
}
public dispose(): void {
super.dispose();
CompositeActionItem.clearDraggedComposite();
this.$label.destroy();
}
}
export class ToggleCompositePinnedAction extends Action {
constructor(
private activity: IActivity,
private compositeBar: ICompositeBar
) {
super('show.toggleCompositePinned', activity ? activity.name : nls.localize('toggle', "Toggle View Pinned"));
this.checked = this.activity && this.compositeBar.isPinned(this.activity.id);
}
public run(context: string): TPromise<any> {
const id = this.activity ? this.activity.id : context;
if (this.compositeBar.isPinned(id)) {
this.compositeBar.unpin(id);
} else {
this.compositeBar.pin(id);
}
return TPromise.as(true);
}
}

View File

@@ -5,14 +5,10 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import types = require('vs/base/common/types');
import { Builder } from 'vs/base/browser/builder';
import { Registry } from 'vs/platform/registry/common/platform';
import { Panel } from 'vs/workbench/browser/panel';
import { EditorInput, EditorOptions, IEditorDescriptor, IEditorInputFactory, IEditorRegistry, Extensions, IFileInputFactory } from 'vs/workbench/common/editor';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { IEditor, Position } from 'vs/platform/editor/common/editor';
import { IInstantiationService, IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation';
import { SyncDescriptor, AsyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -123,183 +119,4 @@ export abstract class BaseEditor extends Panel implements IEditor {
// Super Dispose
super.dispose();
}
}
/**
* A lightweight descriptor of an editor. The descriptor is deferred so that heavy editors
* can load lazily in the workbench.
*/
export class EditorDescriptor extends AsyncDescriptor<BaseEditor> implements IEditorDescriptor {
private id: string;
private name: string;
constructor(id: string, name: string, moduleId: string, ctorName: string) {
super(moduleId, ctorName);
this.id = id;
this.name = name;
}
public getId(): string {
return this.id;
}
public getName(): string {
return this.name;
}
public describes(obj: any): boolean {
return obj instanceof BaseEditor && (<BaseEditor>obj).getId() === this.id;
}
}
const INPUT_DESCRIPTORS_PROPERTY = '__$inputDescriptors';
class EditorRegistry implements IEditorRegistry {
private editors: EditorDescriptor[];
private instantiationService: IInstantiationService;
private fileInputFactory: IFileInputFactory;
private editorInputFactoryConstructors: { [editorInputId: string]: IConstructorSignature0<IEditorInputFactory> } = Object.create(null);
private editorInputFactoryInstances: { [editorInputId: string]: IEditorInputFactory } = Object.create(null);
constructor() {
this.editors = [];
}
public setInstantiationService(service: IInstantiationService): void {
this.instantiationService = service;
for (let key in this.editorInputFactoryConstructors) {
const element = this.editorInputFactoryConstructors[key];
this.createEditorInputFactory(key, element);
}
this.editorInputFactoryConstructors = {};
}
private createEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0<IEditorInputFactory>): void {
const instance = this.instantiationService.createInstance(ctor);
this.editorInputFactoryInstances[editorInputId] = instance;
}
public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor<EditorInput>): void;
public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor<EditorInput>[]): void;
public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: any): void {
// Support both non-array and array parameter
let inputDescriptors: SyncDescriptor<EditorInput>[] = [];
if (!types.isArray(editorInputDescriptor)) {
inputDescriptors.push(editorInputDescriptor);
} else {
inputDescriptors = editorInputDescriptor;
}
// Register (Support multiple Editors per Input)
descriptor[INPUT_DESCRIPTORS_PROPERTY] = inputDescriptors;
this.editors.push(descriptor);
}
public getEditor(input: EditorInput): EditorDescriptor {
const findEditorDescriptors = (input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] => {
const matchingDescriptors: EditorDescriptor[] = [];
for (let i = 0; i < this.editors.length; i++) {
const editor = this.editors[i];
const inputDescriptors = <SyncDescriptor<EditorInput>[]>editor[INPUT_DESCRIPTORS_PROPERTY];
for (let j = 0; j < inputDescriptors.length; j++) {
const inputClass = inputDescriptors[j].ctor;
// Direct check on constructor type (ignores prototype chain)
if (!byInstanceOf && input.constructor === inputClass) {
matchingDescriptors.push(editor);
break;
}
// Normal instanceof check
else if (byInstanceOf && input instanceof inputClass) {
matchingDescriptors.push(editor);
break;
}
}
}
// If no descriptors found, continue search using instanceof and prototype chain
if (!byInstanceOf && matchingDescriptors.length === 0) {
return findEditorDescriptors(input, true);
}
if (byInstanceOf) {
return matchingDescriptors;
}
return matchingDescriptors;
};
const descriptors = findEditorDescriptors(input);
if (descriptors && descriptors.length > 0) {
// Ask the input for its preferred Editor
const preferredEditorId = input.getPreferredEditorId(descriptors.map(d => d.getId()));
if (preferredEditorId) {
return this.getEditorById(preferredEditorId);
}
// Otherwise, first come first serve
return descriptors[0];
}
return null;
}
public getEditorById(editorId: string): EditorDescriptor {
for (let i = 0; i < this.editors.length; i++) {
const editor = this.editors[i];
if (editor.getId() === editorId) {
return editor;
}
}
return null;
}
public getEditors(): EditorDescriptor[] {
return this.editors.slice(0);
}
public setEditors(editorsToSet: EditorDescriptor[]): void {
this.editors = editorsToSet;
}
public getEditorInputs(): any[] {
const inputClasses: any[] = [];
for (let i = 0; i < this.editors.length; i++) {
const editor = this.editors[i];
const editorInputDescriptors = <SyncDescriptor<EditorInput>[]>editor[INPUT_DESCRIPTORS_PROPERTY];
inputClasses.push(...editorInputDescriptors.map(descriptor => descriptor.ctor));
}
return inputClasses;
}
public registerFileInputFactory(factory: IFileInputFactory): void {
this.fileInputFactory = factory;
}
public getFileInputFactory(): IFileInputFactory {
return this.fileInputFactory;
}
public registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0<IEditorInputFactory>): void {
if (!this.instantiationService) {
this.editorInputFactoryConstructors[editorInputId] = ctor;
} else {
this.createEditorInputFactory(editorInputId, ctor);
}
}
public getEditorInputFactory(editorInputId: string): IEditorInputFactory {
return this.editorInputFactoryInstances[editorInputId];
}
}
Registry.add(Extensions.Editors, new EditorRegistry());
}

View File

@@ -64,48 +64,46 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
}
public setInput(input: EditorInput, options?: EditorOptions): TPromise<void> {
const oldInput = this.input;
super.setInput(input, options);
// Detect options
// Return early for same input unless we force to open
const forceOpen = options && options.forceOpen;
// Same Input
if (!forceOpen && input.matches(oldInput)) {
if (!forceOpen && input.matches(this.input)) {
return TPromise.as<void>(null);
}
// Different Input (Reload)
return input.resolve(true).then((resolvedModel: EditorModel) => {
// Otherwise set input and resolve
return super.setInput(input, options).then(() => {
return input.resolve(true).then((resolvedModel: EditorModel) => {
// Assert Model instance
if (!(resolvedModel instanceof BinaryEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as binary'));
}
// Assert Model instance
if (!(resolvedModel instanceof BinaryEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as binary'));
}
// Assert that the current input is still the one we expect. This prevents a race condition when loading takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Assert that the current input is still the one we expect. This prevents a race condition when loading takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Render Input
const model = <BinaryEditorModel>resolvedModel;
ResourceViewer.show(
{ name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag() },
this.binaryContainer,
this.scrollbar,
(resource: URI) => {
this.windowsService.openExternal(resource.toString()).then(didOpen => {
if (!didOpen) {
return this.windowsService.showItemInFolder(resource.fsPath);
}
// Render Input
const model = <BinaryEditorModel>resolvedModel;
ResourceViewer.show(
{ name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag() },
this.binaryContainer,
this.scrollbar,
(resource: URI) => {
this.windowsService.openExternal(resource.toString()).then(didOpen => {
if (!didOpen) {
return this.windowsService.showItemInFolder(resource.fsPath);
}
return void 0;
});
},
(meta) => this.handleMetadataChanged(meta));
return void 0;
});
},
(meta) => this.handleMetadataChanged(meta));
return TPromise.as<void>(null);
return TPromise.as<void>(null);
});
});
}

View File

@@ -10,8 +10,8 @@ import URI from 'vs/base/common/uri';
import { Action, IAction } from 'vs/base/common/actions';
import { IEditorQuickOpenEntry, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
import { StatusbarItemDescriptor, StatusbarAlignment, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, IEditorRegistry, Extensions as EditorExtensions, IEditorInputFactory, SideBySideEditorInput } from 'vs/workbench/common/editor';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } 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';
@@ -22,15 +22,15 @@ import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor';
import { ChangeEncodingAction, ChangeEOLAction, ChangeModeAction, EditorStatus } from 'vs/workbench/browser/parts/editor/editorStatus';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor } from 'vs/workbench/browser/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
import {
CloseEditorsInGroupAction, CloseEditorsInOtherGroupsAction, CloseAllEditorsAction, MoveGroupLeftAction, MoveGroupRightAction, SplitEditorAction, JoinTwoGroupsAction, KeepEditorAction, CloseOtherEditorsInGroupAction, OpenToSideAction, RevertAndCloseEditorAction,
NavigateBetweenGroupsAction, FocusActiveGroupAction, FocusFirstGroupAction, FocusSecondGroupAction, FocusThirdGroupAction, EvenGroupWidthsAction, MaximizeGroupAction, MinimizeOtherGroupsAction, FocusPreviousGroup, FocusNextGroup, ShowEditorsInGroupOneAction,
toEditorQuickOpenEntry, CloseLeftEditorsInGroupAction, CloseRightEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction, OpenNextEditor, OpenPreviousEditor, NavigateBackwardsAction, NavigateForwardAction, ReopenClosedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, NAVIGATE_IN_GROUP_ONE_PREFIX,
toEditorQuickOpenEntry, CloseLeftEditorsInGroupAction, CloseRightEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction, OpenNextEditor, OpenPreviousEditor, NavigateBackwardsAction, NavigateForwardAction, NavigateLastAction, ReopenClosedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, NAVIGATE_IN_GROUP_ONE_PREFIX,
OpenPreviousEditorFromHistoryAction, ShowAllEditorsAction, NAVIGATE_ALL_EDITORS_GROUP_PREFIX, ClearEditorHistoryAction, ShowEditorsInGroupTwoAction, MoveEditorRightInGroupAction, OpenNextEditorInGroup, OpenPreviousEditorInGroup, OpenNextRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorAction,
NAVIGATE_IN_GROUP_TWO_PREFIX, ShowEditorsInGroupThreeAction, NAVIGATE_IN_GROUP_THREE_PREFIX, FocusLastEditorInStackAction, OpenNextRecentlyUsedEditorInGroupAction, MoveEditorToPreviousGroupAction, MoveEditorToNextGroupAction, MoveEditorLeftInGroupAction, ClearRecentFilesAction
} from 'vs/workbench/browser/parts/editor/editorActions';
@@ -39,14 +39,15 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
import { getQuickNavigateHandler, inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh } from 'vs/base/common/platform';
import { GroupOnePicker, GroupTwoPicker, GroupThreePicker, AllEditorsPicker } from 'vs/workbench/browser/parts/editor/editorPicker';
// Register String Editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new EditorDescriptor(
TextResourceEditor,
TextResourceEditor.ID,
nls.localize('textEditor', "Text Editor"),
'vs/workbench/browser/parts/editor/textResourceEditor',
'TextResourceEditor'
),
[
new SyncDescriptor(UntitledEditorInput),
@@ -57,10 +58,9 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
// Register Text Diff Editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new EditorDescriptor(
TextDiffEditor,
TextDiffEditor.ID,
nls.localize('textDiffEditor', "Text Diff Editor"),
'vs/workbench/browser/parts/editor/textDiffEditor',
'TextDiffEditor'
nls.localize('textDiffEditor', "Text Diff Editor")
),
[
new SyncDescriptor(DiffEditorInput)
@@ -70,10 +70,9 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
// Register Binary Resource Diff Editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new EditorDescriptor(
BinaryResourceDiffEditor,
BinaryResourceDiffEditor.ID,
nls.localize('binaryDiffEditor', "Binary Diff Editor"),
'vs/workbench/browser/parts/editor/binaryDiffEditor',
'BinaryResourceDiffEditor'
nls.localize('binaryDiffEditor', "Binary Diff Editor")
),
[
new SyncDescriptor(DiffEditorInput)
@@ -82,10 +81,9 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new EditorDescriptor(
SideBySideEditor,
SideBySideEditor.ID,
nls.localize('sideBySideEditor', "Side by Side Editor"),
'vs/workbench/browser/parts/editor/sideBySideEditor',
'SideBySideEditor'
nls.localize('sideBySideEditor', "Side by Side Editor")
),
[
new SyncDescriptor(SideBySideEditorInput)
@@ -115,7 +113,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory {
const untitledEditorInput = <UntitledEditorInput>editorInput;
// {{SQL CARBON EDIT}}
if (!untitledEditorInput.getResource) {
if (!untitledEditorInput.getResource()) {
return null;
}
@@ -147,7 +145,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory {
}
}
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditorInputFactory(UntitledEditorInput.ID, UntitledEditorInputFactory);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(UntitledEditorInput.ID, UntitledEditorInputFactory);
interface ISerializedSideBySideEditorInput {
name: string;
@@ -167,7 +165,7 @@ class SideBySideEditorInputFactory implements IEditorInputFactory {
const input = <SideBySideEditorInput>editorInput;
if (input.details && input.master) {
const registry = Registry.as<IEditorRegistry>(EditorExtensions.Editors);
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories);
const detailsInputFactory = registry.getEditorInputFactory(input.details.getTypeId());
const masterInputFactory = registry.getEditorInputFactory(input.master.getTypeId());
@@ -194,7 +192,7 @@ class SideBySideEditorInputFactory implements IEditorInputFactory {
public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput {
const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput);
const registry = Registry.as<IEditorRegistry>(EditorExtensions.Editors);
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories);
const detailsInputFactory = registry.getEditorInputFactory(deserialized.detailsTypeId);
const masterInputFactory = registry.getEditorInputFactory(deserialized.masterTypeId);
@@ -211,7 +209,7 @@ class SideBySideEditorInputFactory implements IEditorInputFactory {
}
}
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditorInputFactory(SideBySideEditorInput.ID, SideBySideEditorInputFactory);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(SideBySideEditorInput.ID, SideBySideEditorInputFactory);
// Register Editor Status
const statusBar = Registry.as<IStatusbarRegistry>(StatusExtensions.Statusbar);
@@ -270,8 +268,8 @@ const editorPickerContext = ContextKeyExpr.and(inQuickOpenContext, ContextKeyExp
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/browser/parts/editor/editorPicker',
'GroupOnePicker',
GroupOnePicker,
GroupOnePicker.ID,
NAVIGATE_IN_GROUP_ONE_PREFIX,
editorPickerContextKey,
[
@@ -296,8 +294,8 @@ Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpen
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/browser/parts/editor/editorPicker',
'GroupTwoPicker',
GroupTwoPicker,
GroupTwoPicker.ID,
NAVIGATE_IN_GROUP_TWO_PREFIX,
editorPickerContextKey,
[]
@@ -306,8 +304,8 @@ Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpen
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/browser/parts/editor/editorPicker',
'GroupThreePicker',
GroupThreePicker,
GroupThreePicker.ID,
NAVIGATE_IN_GROUP_THREE_PREFIX,
editorPickerContextKey,
[]
@@ -316,8 +314,8 @@ Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpen
Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/browser/parts/editor/editorPicker',
'AllEditorsPicker',
AllEditorsPicker,
AllEditorsPicker.ID,
NAVIGATE_ALL_EDITORS_GROUP_PREFIX,
editorPickerContextKey,
[
@@ -343,7 +341,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowEditorsInGroupThre
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenNextEditor, OpenNextEditor.ID, OpenNextEditor.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET] } }), 'View: Open Next Editor', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousEditor, OpenPreviousEditor.ID, OpenPreviousEditor.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.PageUp, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET] } }), 'View: Open Previous Editor', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ReopenClosedEditorAction, ReopenClosedEditorAction.ID, ReopenClosedEditorAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_T }), 'View: Reopen Closed Editor', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ClearRecentFilesAction, ClearRecentFilesAction.ID, ClearRecentFilesAction.LABEL), 'View: Clear Recently Opened', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(ClearRecentFilesAction, ClearRecentFilesAction.ID, ClearRecentFilesAction.LABEL), 'File: Clear Recently Opened', nls.localize('file', "File"));
registry.registerWorkbenchAction(new SyncActionDescriptor(KeepEditorAction, KeepEditorAction.ID, KeepEditorAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter) }), 'View: Keep Editor', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_W) }), 'View: Close All Editors', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseLeftEditorsInGroupAction, CloseLeftEditorsInGroupAction.ID, CloseLeftEditorsInGroupAction.LABEL), 'View: Close Editors to the Left', category);
@@ -373,6 +371,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousGroup, Fo
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextGroup, FocusNextGroup.ID, FocusNextGroup.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.RightArrow) }), 'View: Focus Next Group', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateForwardAction, NavigateForwardAction.ID, NavigateForwardAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.RightArrow }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_MINUS } }), 'Go Forward');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateBackwardsAction, NavigateBackwardsAction.ID, NavigateBackwardsAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }), 'Go Back');
registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateLastAction, NavigateLastAction.ID, NavigateLastAction.LABEL), 'Go Last');
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousEditorFromHistoryAction, OpenPreviousEditorFromHistoryAction.ID, OpenPreviousEditorFromHistoryAction.LABEL), 'Open Previous Editor from History');
registry.registerWorkbenchAction(new SyncActionDescriptor(ClearEditorHistoryAction, ClearEditorHistoryAction.ID, ClearEditorHistoryAction.LABEL), 'Clear Editor History');
registry.registerWorkbenchAction(new SyncActionDescriptor(RevertAndCloseEditorAction, RevertAndCloseEditorAction.ID, RevertAndCloseEditorAction.LABEL), 'View: Revert and Close Editor', category);
@@ -380,8 +379,8 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(RevertAndCloseEditorAc
// Register Editor Picker Actions including quick navigate support
const openNextEditorKeybinding = { primary: KeyMod.CtrlCmd | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyCode.Tab } };
const openPreviousEditorKeybinding = { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } };
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenNextRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction.ID, OpenNextRecentlyUsedEditorInGroupAction.LABEL, openNextEditorKeybinding), 'Open Next Recently Used Editor in Group');
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousRecentlyUsedEditorInGroupAction, OpenPreviousRecentlyUsedEditorInGroupAction.ID, OpenPreviousRecentlyUsedEditorInGroupAction.LABEL, openPreviousEditorKeybinding), 'Open Previous Recently Used Editor in Group');
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenNextRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction.ID, OpenNextRecentlyUsedEditorInGroupAction.LABEL, openNextEditorKeybinding), 'View: Open Next Recently Used Editor in Group', category);
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousRecentlyUsedEditorInGroupAction, OpenPreviousRecentlyUsedEditorInGroupAction.ID, OpenPreviousRecentlyUsedEditorInGroupAction.LABEL, openPreviousEditorKeybinding), 'View: Open Previous Recently Used Editor in Group', category);
const quickOpenNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker';
KeybindingsRegistry.registerCommandAndKeybindingRule({
@@ -405,4 +404,17 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
// Editor Commands
editorCommands.setup();
editorCommands.setup();
// Touch Bar
if (isMacintosh) {
MenuRegistry.appendMenuItem(MenuId.TouchBarContext, {
command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, iconPath: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/back-tb.png')).fsPath },
group: 'navigation'
});
MenuRegistry.appendMenuItem(MenuId.TouchBarContext, {
command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, iconPath: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/forward-tb.png')).fsPath },
group: 'navigation'
});
}

View File

@@ -9,7 +9,7 @@ import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { mixin } from 'vs/base/common/objects';
import { getCodeEditor } from 'vs/editor/common/services/codeEditorService';
import { EditorInput, hasResource, TextEditorOptions, EditorOptions, IEditorIdentifier, IEditorContext, ActiveEditorMoveArguments, ActiveEditorMovePositioning, EditorCommands, ConfirmResult } from 'vs/workbench/common/editor';
import { EditorInput, TextEditorOptions, EditorOptions, IEditorIdentifier, IEditorContext, ActiveEditorMoveArguments, ActiveEditorMovePositioning, EditorCommands, ConfirmResult } from 'vs/workbench/common/editor';
import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -255,16 +255,13 @@ export class FocusFirstGroupAction extends Action {
// Since no editor is currently opened, try to open last history entry to the target side
const history = this.historyService.getHistory();
for (let input of history) {
// For now only support to open files from history to the side
if (history.length > 0) {
const input = history[0];
if (input instanceof EditorInput) {
if (hasResource(input, { filter: ['file', 'untitled'] })) {
return this.editorService.openEditor(input, null, Position.ONE);
}
} else {
return this.editorService.openEditor(input as IResourceInput, Position.ONE);
return this.editorService.openEditor(input, null, Position.ONE);
}
return this.editorService.openEditor(input as IResourceInput, Position.ONE);
}
return TPromise.as(true);
@@ -327,15 +324,11 @@ export abstract class BaseFocusSideGroupAction extends Action {
else if (referenceEditor) {
const history = this.historyService.getHistory();
for (let input of history) {
// For now only support to open files from history to the side
if (input instanceof EditorInput) {
if (hasResource(input, { filter: ['file', 'untitled'] })) {
return this.editorService.openEditor(input, { pinned: true }, this.getTargetEditorSide());
}
} else {
return this.editorService.openEditor({ resource: (input as IResourceInput).resource, options: { pinned: true } }, this.getTargetEditorSide());
if (input instanceof EditorInput && input.supportsSplitEditor()) {
return this.editorService.openEditor(input, { pinned: true }, this.getTargetEditorSide());
}
return this.editorService.openEditor({ resource: (input as IResourceInput).resource, options: { pinned: true } }, this.getTargetEditorSide());
}
}
@@ -1116,6 +1109,22 @@ export class NavigateBackwardsAction extends Action {
}
}
export class NavigateLastAction extends Action {
public static ID = 'workbench.action.navigateLast';
public static LABEL = nls.localize('navigateLast', "Go Last");
constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) {
super(id, label);
}
public run(): TPromise<any> {
this.historyService.last();
return TPromise.as(null);
}
}
export class ReopenClosedEditorAction extends Action {
public static ID = 'workbench.action.reopenClosedEditor';

View File

@@ -199,7 +199,7 @@ function handleCommandDeprecations(): void {
};
Object.keys(mapDeprecatedCommands).forEach(deprecatedCommandId => {
const newCommandId = mapDeprecatedCommands[deprecatedCommandId];
const newCommandId: string = mapDeprecatedCommands[deprecatedCommandId];
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: deprecatedCommandId,

View File

@@ -20,7 +20,7 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import { isMacintosh } from 'vs/base/common/platform';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { Position, POSITIONS } from 'vs/platform/editor/common/editor';
import { IEditorGroupService, ITabOptions, GroupArrangement, GroupOrientation } from 'vs/workbench/services/group/common/groupService';
import { IEditorGroupService, IEditorTabOptions, GroupArrangement, GroupOrientation } from 'vs/workbench/services/group/common/groupService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
@@ -73,6 +73,7 @@ export interface IEditorGroupsControl {
getInstantiationService(position: Position): IInstantiationService;
getProgressBar(position: Position): ProgressBar;
updateProgress(position: Position, state: ProgressState): void;
updateTitleAreas(refreshActive?: boolean): void;
layout(dimension: Dimension): void;
layout(position: Position): void;
@@ -115,7 +116,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
private layoutVertically: boolean;
private tabOptions: ITabOptions;
private tabOptions: IEditorTabOptions;
private silos: Builder[];
private silosSize: number[];
@@ -225,7 +226,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
this.extensionService.onReady().then(() => this.onExtensionsReady());
}
private updateTabOptions(tabOptions: ITabOptions, refresh?: boolean): void {
private updateTabOptions(tabOptions: IEditorTabOptions, refresh?: boolean): void {
const tabCloseButton = this.tabOptions ? this.tabOptions.tabCloseButton : 'right';
this.tabOptions = tabOptions;
@@ -443,6 +444,9 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
// Log this fact in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchEditorMaximized" : {}
*/
this.telemetryService.publicLog('workbenchEditorMaximized');
}
@@ -2077,6 +2081,32 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
return silo ? silo.child().getProperty(key) : void 0;
}
public updateTitleAreas(refreshActive?: boolean): void {
POSITIONS.forEach(position => {
const group = this.stacks.groupAt(position);
if (!group) {
return;
}
const titleControl = this.getTitleAreaControl(position);
if (!titleControl) {
return;
}
// Make sure the active group is shown in the title
// and refresh it if we are instructed to refresh it
if (refreshActive && group.isActive) {
titleControl.setContext(group);
titleControl.refresh(true);
}
// Otherwise, just refresh the toolbar
else {
titleControl.updateEditorActionsToolbar();
}
});
}
public updateProgress(position: Position, state: ProgressState): void {
const progressbar = this.getProgressBar(position);
if (!progressbar) {

View File

@@ -20,17 +20,17 @@ import { getCodeEditor } from 'vs/editor/common/services/codeEditorService';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Scope as MementoScope } from 'vs/workbench/common/memento';
import { Part } from 'vs/workbench/browser/part';
import { BaseEditor, EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IEditorRegistry, Extensions as EditorExtensions, EditorInput, EditorOptions, ConfirmResult, IWorkbenchEditorConfiguration, IEditorDescriptor, TextEditorOptions, SideBySideEditorInput, TextCompareEditorVisible, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, ConfirmResult, IWorkbenchEditorConfiguration, TextEditorOptions, SideBySideEditorInput, TextCompareEditorVisible, TEXT_DIFF_EDITOR_ID, EditorOpeningEvent, IEditorOpeningEvent } from 'vs/workbench/common/editor';
import { EditorGroupsControl, Rochade, IEditorGroupsControl, ProgressState } from 'vs/workbench/browser/parts/editor/editorGroupsControl';
import { WorkbenchProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { IEditorGroupService, GroupOrientation, GroupArrangement, ITabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEditorPart } from 'vs/workbench/services/editor/browser/editorService';
import { IEditorGroupService, GroupOrientation, GroupArrangement, IEditorTabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IEditorPart } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { Position, POSITIONS, Direction } from 'vs/platform/editor/common/editor';
import { Position, POSITIONS, Direction, IEditor } from 'vs/platform/editor/common/editor';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IMessageService, IMessageWithAction, Severity } from 'vs/platform/message/common/message';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -44,10 +44,13 @@ import { EDITOR_GROUP_BACKGROUND } from 'vs/workbench/common/theme';
import { createCSSRule } from 'vs/base/browser/dom';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join } from 'vs/base/common/paths';
import { isCommonCodeEditor } from 'vs/editor/common/editorCommon';
import { IEditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
// {{SQL CARBON EDIT}}
import { convertEditorInput } from 'sql/parts/common/customInputConverter';
class ProgressMonitor {
constructor(private _token: number, private progressPromise: TPromise<void>) { }
@@ -94,23 +97,23 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
private editorGroupsControl: IEditorGroupsControl;
private memento: object;
private stacks: EditorStacksModel;
private tabOptions: ITabOptions;
private tabOptions: IEditorTabOptions;
private forceHideTabs: boolean;
private doNotFireTabOptionsChanged: boolean;
private revealIfOpen: boolean;
private _onEditorsChanged: Emitter<void>;
private _onEditorsMoved: Emitter<void>;
private _onEditorOpening: Emitter<IEditorOpeningEvent>;
private _onEditorGroupMoved: Emitter<void>;
private _onEditorOpenFail: Emitter<EditorInput>;
private _onGroupOrientationChanged: Emitter<void>;
private _onTabOptionsChanged: Emitter<ITabOptions>;
private _onTabOptionsChanged: Emitter<IEditorTabOptions>;
private textCompareEditorVisible: IContextKey<boolean>;
// The following data structures are partitioned into array of Position as provided by Services.POSITION array
private visibleEditors: BaseEditor[];
private instantiatedEditors: BaseEditor[][];
private mapEditorInstantiationPromiseToEditor: { [editorId: string]: TPromise<BaseEditor>; }[];
private editorOpenToken: number[];
private pendingEditorInputsToClose: EditorIdentifier[];
private pendingEditorInputCloseTimeout: number;
@@ -134,10 +137,11 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
super(id, { hasTitle: false }, themeService);
this._onEditorsChanged = new Emitter<void>();
this._onEditorsMoved = new Emitter<void>();
this._onEditorOpening = new Emitter<IEditorOpeningEvent>();
this._onEditorGroupMoved = new Emitter<void>();
this._onEditorOpenFail = new Emitter<EditorInput>();
this._onGroupOrientationChanged = new Emitter<void>();
this._onTabOptionsChanged = new Emitter<ITabOptions>();
this._onTabOptionsChanged = new Emitter<IEditorTabOptions>();
this.visibleEditors = [];
@@ -145,8 +149,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
this.instantiatedEditors = arrays.fill(POSITIONS.length, () => []);
this.mapEditorInstantiationPromiseToEditor = arrays.fill(POSITIONS.length, () => Object.create(null));
this.pendingEditorInputsToClose = [];
this.pendingEditorInputCloseTimeout = null;
@@ -162,18 +164,27 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
previewEditors: editorConfig.enablePreview,
showIcons: editorConfig.showIcons,
showTabs: editorConfig.showTabs,
tabCloseButton: editorConfig.tabCloseButton
tabCloseButton: editorConfig.tabCloseButton,
labelFormat: editorConfig.labelFormat,
};
this.revealIfOpen = editorConfig.revealIfOpen;
/* __GDPR__
"workbenchEditorConfiguration" : {
"${include}": [
"${IWorkbenchEditorConfiguration}"
]
}
*/
this.telemetryService.publicLog('workbenchEditorConfiguration', editorConfig);
} else {
this.tabOptions = {
previewEditors: true,
showIcons: false,
showTabs: true,
tabCloseButton: 'right'
tabCloseButton: 'right',
labelFormat: 'default',
};
this.revealIfOpen = false;
@@ -199,40 +210,44 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
this.toUnbind.push(this.stacks.onEditorClosed(event => this.onEditorClosed(event)));
this.toUnbind.push(this.stacks.onGroupOpened(event => this.onEditorGroupOpenedOrClosed()));
this.toUnbind.push(this.stacks.onGroupClosed(event => this.onEditorGroupOpenedOrClosed()));
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(this.configurationService.getConfiguration<IWorkbenchEditorConfiguration>())));
this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
}
private onEditorGroupOpenedOrClosed(): void {
this.updateStyles();
}
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
if (configuration && configuration.workbench && configuration.workbench.editor) {
const editorConfig = configuration.workbench.editor;
private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
if (event.affectsConfiguration('workbench.editor')) {
const configuration = this.configurationService.getConfiguration<IWorkbenchEditorConfiguration>();
if (configuration && configuration.workbench && configuration.workbench.editor) {
const editorConfig = configuration.workbench.editor;
// Pin all preview editors of the user chose to disable preview
const newPreviewEditors = editorConfig.enablePreview;
if (this.tabOptions.previewEditors !== newPreviewEditors && !newPreviewEditors) {
this.stacks.groups.forEach(group => {
if (group.previewEditor) {
this.pinEditor(group, group.previewEditor);
}
});
// Pin all preview editors of the user chose to disable preview
const newPreviewEditors = editorConfig.enablePreview;
if (this.tabOptions.previewEditors !== newPreviewEditors && !newPreviewEditors) {
this.stacks.groups.forEach(group => {
if (group.previewEditor) {
this.pinEditor(group, group.previewEditor);
}
});
}
const oldTabOptions = objects.clone(this.tabOptions);
this.tabOptions = {
previewEditors: newPreviewEditors,
showIcons: editorConfig.showIcons,
tabCloseButton: editorConfig.tabCloseButton,
showTabs: this.forceHideTabs ? false : editorConfig.showTabs,
labelFormat: editorConfig.labelFormat,
};
if (!this.doNotFireTabOptionsChanged && !objects.equals(oldTabOptions, this.tabOptions)) {
this._onTabOptionsChanged.fire(this.tabOptions);
}
this.revealIfOpen = editorConfig.revealIfOpen;
}
const oldTabOptions = objects.clone(this.tabOptions);
this.tabOptions = {
previewEditors: newPreviewEditors,
showIcons: editorConfig.showIcons,
tabCloseButton: editorConfig.tabCloseButton,
showTabs: this.forceHideTabs ? false : editorConfig.showTabs
};
if (!this.doNotFireTabOptionsChanged && !objects.equals(oldTabOptions, this.tabOptions)) {
this._onTabOptionsChanged.fire(this.tabOptions);
}
this.revealIfOpen = editorConfig.revealIfOpen;
}
}
@@ -248,10 +263,24 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
}
private onEditorOpened(identifier: EditorIdentifier): void {
/* __GDPR__
"editorOpened" : {
"${include}": [
"${EditorTelemetryDescriptor}"
]
}
*/
this.telemetryService.publicLog('editorOpened', identifier.editor.getTelemetryDescriptor());
}
private onEditorClosed(event: EditorCloseEvent): void {
/* __GDPR__
"editorClosed" : {
"${include}": [
"${EditorTelemetryDescriptor}"
]
}
*/
this.telemetryService.publicLog('editorClosed', event.editor.getTelemetryDescriptor());
}
@@ -270,8 +299,12 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return this._onEditorsChanged.event;
}
public get onEditorsMoved(): Event<void> {
return this._onEditorsMoved.event;
public get onEditorOpening(): Event<IEditorOpeningEvent> {
return this._onEditorOpening.event;
}
public get onEditorGroupMoved(): Event<void> {
return this._onEditorGroupMoved.event;
}
public get onEditorOpenFail(): Event<EditorInput> {
@@ -282,39 +315,52 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return this._onGroupOrientationChanged.event;
}
public get onTabOptionsChanged(): Event<ITabOptions> {
public get onTabOptionsChanged(): Event<IEditorTabOptions> {
return this._onTabOptionsChanged.event;
}
public getTabOptions(): ITabOptions {
public getTabOptions(): IEditorTabOptions {
return this.tabOptions;
}
public openEditor(input: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<BaseEditor>;
public openEditor(input: EditorInput, options?: EditorOptions, position?: Position, ratio?: number[]): TPromise<BaseEditor>;
public openEditor(input: EditorInput, options?: EditorOptions, arg3?: any, ratio?: number[]): TPromise<BaseEditor> {
// Normalize some values
if (!options) { options = null; }
public openEditor(input: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
public openEditor(input: EditorInput, options?: EditorOptions, position?: Position, ratio?: number[]): TPromise<IEditor>;
public openEditor(input: EditorInput, options?: EditorOptions, arg3?: any, ratio?: number[]): TPromise<IEditor> {
if (!options) {
options = null;
}
// Determine position to open editor in (one, two, three)
const position = this.findPosition(input, options, arg3, ratio);
// Some conditions under which we prevent the request
if (
!input || // no input
position === null || // invalid position
Object.keys(this.mapEditorInstantiationPromiseToEditor[position]).length > 0 || // pending editor load
!this.editorGroupsControl || // too early
this.editorGroupsControl.isDragging() // pending editor DND
!input || // no input
position === null || // invalid position
!this.editorGroupsControl || // too early
this.editorGroupsControl.isDragging() // pending editor DND
) {
return TPromise.as<BaseEditor>(null);
}
// Editor opening event (can be prevented and overridden)
const event = new EditorOpeningEvent(input, options, position);
this._onEditorOpening.fire(event);
const prevented = event.isPrevented();
if (prevented) {
return prevented();
}
// {{SQL CARBON EDIT}}
// Convert input into custom type if it's one of the ones we support
input = convertEditorInput(input, options, this.instantiationService);
// Open through UI
return this.doOpenEditor(position, input, options, ratio);
}
private doOpenEditor(position: Position, input: EditorInput, options: EditorOptions, ratio: number[]): TPromise<BaseEditor> {
// We need an editor descriptor for the input
const descriptor = Registry.as<IEditorRegistry>(EditorExtensions.Editors).getEditor(input);
if (!descriptor) {
@@ -323,15 +369,14 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Opened to the side
if (position !== Position.ONE) {
/* __GDPR__
"workbenchSideEditorOpened" : {
"position" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchSideEditorOpened', { position: position });
}
// Open through UI
return this.doOpenEditor(position, descriptor, input, options, ratio);
}
private doOpenEditor(position: Position, descriptor: IEditorDescriptor, input: EditorInput, options: EditorOptions, ratio: number[]): TPromise<BaseEditor> {
// Update stacks: We do this early on before the UI is there because we want our stacks model to have
// a consistent view of the editor world and updating it later async after the UI is there will cause
// issues (e.g. when a closeEditor call is made that expects the openEditor call to have updated the
@@ -339,7 +384,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// This can however cause a race condition where the stacks model indicates the opened editor is there
// while the UI is not yet ready. Clients have to deal with this fact and we have to make sure that the
// stacks model gets updated if any of the UI updating fails with an error.
const group = this.ensureGroup(position, !options || !options.preserveFocus);
const [group, newGroupOpened] = this.ensureGroup(position, !options || !options.preserveFocus);
const pinned = !this.tabOptions.previewEditors || (options && (options.pinned || typeof options.index === 'number')) || input.isDirty();
const active = (group.count === 0) || !options || !options.inactive;
@@ -362,23 +407,34 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
}));
// Show editor
return this.doShowEditor(group, descriptor, input, options, ratio, monitor).then(editor => {
if (!editor) {
return TPromise.as<BaseEditor>(null); // canceled or other error
}
const editor = this.doShowEditor(group, descriptor, input, options, ratio, monitor);
if (!editor) {
return TPromise.as<BaseEditor>(null); // canceled or other error
}
// Set input to editor
return this.doSetInput(group, editor, input, options, monitor);
});
// Set input to editor
const inputPromise = this.doSetInput(group, editor, input, options, monitor);
// A new active group got opened. Since this involves updating the title area controls to show
// the new editor and actions we trigger a direct update of title controls from here to avoid
// some UI flickering if we rely on the event handlers that all use schedulers.
// The reason we can trigger this now is that after the input is set to the editor group, the
// resource context is updated and the correct number of actions will be resolved from the title
// area.
if (newGroupOpened && this.stacks.isActive(group)) {
this.editorGroupsControl.updateTitleAreas(true /* refresh new active group */);
}
return inputPromise;
}
private doShowEditor(group: EditorGroup, descriptor: IEditorDescriptor, input: EditorInput, options: EditorOptions, ratio: number[], monitor: ProgressMonitor): TPromise<BaseEditor> {
const position = this.stacks.positionOfGroup(group);
private doShowEditor(group: EditorGroup, descriptor: IEditorDescriptor, input: EditorInput, options: EditorOptions, ratio: number[], monitor: ProgressMonitor): BaseEditor {
let position = this.stacks.positionOfGroup(group);
const editorAtPosition = this.visibleEditors[position];
// Return early if the currently visible editor can handle the input
if (editorAtPosition && descriptor.describes(editorAtPosition)) {
return TPromise.as(editorAtPosition);
return editorAtPosition;
}
// Hide active one first
@@ -387,107 +443,77 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
}
// Create Editor
return this.doCreateEditor(group, descriptor, monitor).then(editor => {
const position = this.stacks.positionOfGroup(group); // might have changed due to a rochade meanwhile
const editor = this.doCreateEditor(group, descriptor, monitor);
position = this.stacks.positionOfGroup(group); // might have changed due to a rochade meanwhile
// Make sure that the user meanwhile did not open another editor or something went wrong
if (!editor || !this.visibleEditors[position] || editor.getId() !== this.visibleEditors[position].getId()) {
monitor.cancel();
// Make sure that the user meanwhile did not open another editor or something went wrong
if (!editor || !this.visibleEditors[position] || editor.getId() !== this.visibleEditors[position].getId()) {
monitor.cancel();
return null;
}
// Show in side by side control
this.editorGroupsControl.show(editor, position, options && options.preserveFocus, ratio);
// Indicate to editor that it is now visible
editor.setVisible(true, position);
// Update text compare editor visible context
this.updateTextCompareEditorVisible();
// Make sure the editor is layed out
this.editorGroupsControl.layout(position);
return editor;
}, e => {
this.messageService.show(Severity.Error, types.isString(e) ? new Error(e) : e);
return null;
});
}
// Show in side by side control
this.editorGroupsControl.show(editor, position, options && options.preserveFocus, ratio);
// Indicate to editor that it is now visible
editor.setVisible(true, position);
// Update text compare editor visible context
this.updateTextCompareEditorVisible();
// Make sure the editor is layed out
this.editorGroupsControl.layout(position);
return editor;
}
private doCreateEditor(group: EditorGroup, descriptor: IEditorDescriptor, monitor: ProgressMonitor): TPromise<BaseEditor> {
private doCreateEditor(group: EditorGroup, descriptor: IEditorDescriptor, monitor: ProgressMonitor): BaseEditor {
// Instantiate editor
return this.doInstantiateEditor(group, descriptor).then(editor => {
const position = this.stacks.positionOfGroup(group); // might have changed due to a rochade meanwhile
const editor = this.doInstantiateEditor(group, descriptor);
const position = this.stacks.positionOfGroup(group); // might have changed due to a rochade meanwhile
// Make sure that the user meanwhile did not open another editor
if (monitor.token !== this.editorOpenToken[position]) {
monitor.cancel();
// Make sure that the user meanwhile did not open another editor
if (monitor.token !== this.editorOpenToken[position]) {
monitor.cancel();
return null;
}
return null;
}
// Remember Editor at position
this.visibleEditors[position] = editor;
// Remember Editor at position
this.visibleEditors[position] = editor;
// Create editor as needed
if (!editor.getContainer()) {
editor.create($().div({
'class': 'editor-container',
'role': 'tabpanel',
id: descriptor.getId()
}));
}
// Create editor as needed
if (!editor.getContainer()) {
editor.create($().div({
'class': 'editor-container',
'role': 'tabpanel',
id: descriptor.getId()
}));
}
return editor;
});
return editor;
}
private doInstantiateEditor(group: EditorGroup, descriptor: IEditorDescriptor): TPromise<BaseEditor> {
private doInstantiateEditor(group: EditorGroup, descriptor: IEditorDescriptor): BaseEditor {
const position = this.stacks.positionOfGroup(group);
// Return early if already instantiated
const instantiatedEditor = this.instantiatedEditors[position].filter(e => descriptor.describes(e))[0];
if (instantiatedEditor) {
return TPromise.as(instantiatedEditor);
}
// Return early if editor is being instantiated at the same time from a previous call
const pendingEditorInstantiate = this.mapEditorInstantiationPromiseToEditor[position][descriptor.getId()];
if (pendingEditorInstantiate) {
return pendingEditorInstantiate;
return instantiatedEditor;
}
// Otherwise instantiate
const progressService = this.instantiationService.createInstance(WorkbenchProgressService, this.editorGroupsControl.getProgressBar(position), descriptor.getId(), true);
const editorInstantiationService = this.editorGroupsControl.getInstantiationService(position).createChild(new ServiceCollection([IProgressService, progressService]));
let loaded = false;
const onInstantiate = (arg: BaseEditor): TPromise<BaseEditor> => {
const position = this.stacks.positionOfGroup(group); // might have changed due to a rochade meanwhile
const editor = descriptor.instantiate(editorInstantiationService);
loaded = true;
delete this.mapEditorInstantiationPromiseToEditor[position][descriptor.getId()];
this.instantiatedEditors[position].push(editor);
this.instantiatedEditors[position].push(arg);
return TPromise.as(arg);
};
const instantiateEditorPromise = editorInstantiationService.createInstance(<EditorDescriptor>descriptor).then(onInstantiate);
if (!loaded) {
this.mapEditorInstantiationPromiseToEditor[position][descriptor.getId()] = instantiateEditorPromise;
}
return instantiateEditorPromise.then(result => {
progressService.dispose();
return result;
});
return editor;
}
private doSetInput(group: EditorGroup, editor: BaseEditor, input: EditorInput, options: EditorOptions, monitor: ProgressMonitor): TPromise<BaseEditor> {
@@ -641,6 +667,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Explicitly trigger the focus changed handler because the side by side control will not trigger it unless
// the user is actively changing focus with the mouse from left/top to right/bottom.
this.onGroupFocusChanged();
// Update title area sync to avoid some flickering with actions
this.editorGroupsControl.updateTitleAreas();
}
}
@@ -667,7 +696,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Emit Editor move event
if (rochade !== Rochade.NONE) {
this._onEditorsMoved.fire();
this._onEditorGroupMoved.fire();
}
}
@@ -875,14 +904,13 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Move data structures
arrays.move(this.visibleEditors, fromPosition, toPosition);
arrays.move(this.editorOpenToken, fromPosition, toPosition);
arrays.move(this.mapEditorInstantiationPromiseToEditor, fromPosition, toPosition);
arrays.move(this.instantiatedEditors, fromPosition, toPosition);
// Restore focus
this.focusGroup(fromGroup);
// Events
this._onEditorsMoved.fire();
this._onEditorGroupMoved.fire();
}
public moveEditor(input: EditorInput, from: EditorGroup, to: EditorGroup, moveOptions?: IMoveOptions): void;
@@ -1020,7 +1048,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
}
}
public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: EditorOptions }[], position?: Position): TPromise<BaseEditor[]> {
public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: EditorOptions }[], position?: Position): TPromise<IEditor[]> {
const activeReplacements: IEditorReplacement[] = [];
const hiddenReplacements: IEditorReplacement[] = [];
@@ -1079,9 +1107,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return res;
}
public openEditors(editors: { input: EditorInput, position: Position, options?: EditorOptions }[]): TPromise<BaseEditor[]> {
public openEditors(editors: { input: EditorInput, position: Position, options?: EditorOptions }[]): TPromise<IEditor[]> {
if (!editors.length) {
return TPromise.as<BaseEditor[]>([]);
return TPromise.as<IEditor[]>([]);
}
let activePosition: Position;
@@ -1098,7 +1126,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return this.stacks.groups.some(g => g.count > 0);
}
public restoreEditors(): TPromise<BaseEditor[]> {
public restoreEditors(): TPromise<IEditor[]> {
const editors = this.stacks.groups.map((group, index) => {
return {
input: group.activeEditor,
@@ -1108,7 +1136,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
});
if (!editors.length) {
return TPromise.as<BaseEditor[]>([]);
return TPromise.as<IEditor[]>([]);
}
let activePosition: Position;
@@ -1121,7 +1149,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
return this.doOpenEditors(editors, activePosition, editorState && editorState.ratio);
}
private doOpenEditors(editors: { input: EditorInput, position: Position, options?: EditorOptions }[], activePosition?: number, ratio?: number[]): TPromise<BaseEditor[]> {
private doOpenEditors(editors: { input: EditorInput, position: Position, options?: EditorOptions }[], activePosition?: number, ratio?: number[]): TPromise<IEditor[]> {
const positionOneEditors = editors.filter(e => e.position === Position.ONE);
const positionTwoEditors = editors.filter(e => e.position === Position.TWO);
const positionThreeEditors = editors.filter(e => e.position === Position.THREE);
@@ -1168,7 +1196,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Open each input respecting the options. Since there can only be one active editor in each
// position, we have to pick the first input from each position and add the others as inactive
const promises: TPromise<BaseEditor>[] = [];
const promises: TPromise<IEditor>[] = [];
[positionOneEditors.shift(), positionTwoEditors.shift(), positionThreeEditors.shift()].forEach((editor, position) => {
if (!editor) {
return; // unused position
@@ -1177,7 +1205,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
const input = editor.input;
// Resolve editor options
const preserveFocus = (activePosition !== position);
const preserveFocus = (activePosition !== position && ratio && ratio.length > 0); // during restore, preserve focus to reduce flicker
let options: EditorOptions;
if (editor.options) {
options = editor.options;
@@ -1202,7 +1230,12 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
[positionOneEditors, positionTwoEditors, positionThreeEditors].forEach((editors, index) => {
const group = this.stacks.groupAt(index);
if (group) {
editors.forEach(editor => group.openEditor(editor.input, { pinned: true })); // group could be null if one openeditor call failed!
// Make sure we are keeping the order as the editors are passed to us. We have to set
// an explicit index because otherwise we would put editors in the wrong order
// (see https://github.com/Microsoft/vscode/issues/30364)
const startingIndex = group.indexOf(group.activeEditor) + 1;
editors.forEach((editor, offset) => group.openEditor(editor.input, { pinned: true, index: (startingIndex + offset) }));
}
});
@@ -1298,6 +1331,20 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
}
}
public invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T {
const activeEditor = this.getActiveEditor();
if (activeEditor) {
const activeEditorControl = activeEditor.getControl();
if (isCommonCodeEditor(activeEditorControl)) {
return activeEditorControl.invokeWithinContext(fn);
}
return this.editorGroupsControl.getInstantiationService(activeEditor.position).invokeFunction(fn);
}
return this.instantiationService.invokeFunction(fn);
}
public layout(dimension: Dimension): Dimension[] {
// Pass to super
@@ -1337,7 +1384,8 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Emitters
this._onEditorsChanged.dispose();
this._onEditorsMoved.dispose();
this._onEditorOpening.dispose();
this._onEditorGroupMoved.dispose();
this._onEditorOpenFail.dispose();
// Reset Tokens
@@ -1493,7 +1541,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
this.doRochade(this.visibleEditors, from, to, null);
this.doRochade(this.editorOpenToken, from, to, null);
this.doRochade(this.mapEditorInstantiationPromiseToEditor, from, to, Object.create(null));
this.doRochade(this.instantiatedEditors, from, to, []);
}
}
@@ -1503,9 +1550,11 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
array[from] = empty;
}
private ensureGroup(position: Position, activate = true): EditorGroup {
private ensureGroup(position: Position, activate = true): [EditorGroup, boolean /* new group opened */] {
let newGroupOpened = false;
let group = this.stacks.groupAt(position);
if (!group) {
newGroupOpened = true;
// Race condition: it could be that someone quickly opens editors one after
// the other and we are asked to open an editor in position 2 before position
@@ -1528,7 +1577,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
this.stacks.setActive(group);
}
return group;
return [group, newGroupOpened];
}
private modifyGroups(modification: () => void) {

View File

@@ -7,14 +7,11 @@
import 'vs/css!./media/editorpicker';
import { TPromise } from 'vs/base/common/winjs.base';
import nls = require('vs/nls');
import labels = require('vs/base/common/labels');
import URI from 'vs/base/common/uri';
import errors = require('vs/base/common/errors');
import strings = require('vs/base/common/strings');
import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IAutoFocus, Mode, IEntryRunContext, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import scorer = require('vs/base/common/scorer');
import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup, QuickOpenItemAccessor } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { IModeService } from 'vs/editor/common/services/modeService';
import { getIconClasses } from 'vs/workbench/browser/labels';
import { IModelService } from 'vs/editor/common/services/modelService';
@@ -25,6 +22,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { EditorInput, toResource, IEditorGroup, IEditorStacksModel } from 'vs/workbench/common/editor';
import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
export class EditorPickerEntry extends QuickOpenEntryGroup {
private stacks: IEditorStacksModel;
@@ -89,7 +87,7 @@ export class EditorPickerEntry extends QuickOpenEntryGroup {
}
export abstract class BaseEditorPicker extends QuickOpenHandler {
private scorerCache: { [key: string]: number };
private scorerCache: ScorerCache;
constructor(
@IInstantiationService protected instantiationService: IInstantiationService,
@@ -103,41 +101,38 @@ export abstract class BaseEditorPicker extends QuickOpenHandler {
}
public getResults(searchValue: string): TPromise<QuickOpenModel> {
searchValue = searchValue.trim();
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
const editorEntries = this.getEditorEntries();
if (!editorEntries.length) {
return TPromise.as(null);
}
const stacks = this.editorGroupService.getStacksModel();
// Prepare search for scoring
const query = prepareQuery(searchValue);
const entries = editorEntries.filter(e => {
if (!searchValue) {
if (!query.value) {
return true;
}
const resource = e.getResource();
const targetToMatch = resource ? labels.getPathLabel(e.getResource(), this.contextService) : e.getLabel();
if (!scorer.matches(targetToMatch, normalizedSearchValueLowercase)) {
const itemScore = scoreItem(e, query, true, QuickOpenItemAccessor, this.scorerCache);
if (!itemScore.score) {
return false;
}
const { labelHighlights, descriptionHighlights } = QuickOpenEntry.highlight(e, searchValue, true /* fuzzy highlight */);
e.setHighlights(labelHighlights, descriptionHighlights);
e.setHighlights(itemScore.labelMatch, itemScore.descriptionMatch);
return true;
});
// Sorting
if (searchValue) {
const stacks = this.editorGroupService.getStacksModel();
if (query.value) {
entries.sort((e1, e2) => {
if (e1.group !== e2.group) {
return stacks.positionOfGroup(e1.group) - stacks.positionOfGroup(e2.group);
}
return QuickOpenEntry.compareByScore(e1, e2, searchValue, normalizedSearchValueLowercase, this.scorerCache);
return compareItemsByScore(e1, e2, query, true, QuickOpenItemAccessor, this.scorerCache);
});
}
@@ -220,6 +215,8 @@ export abstract class EditorGroupPicker extends BaseEditorPicker {
export class GroupOnePicker extends EditorGroupPicker {
public static readonly ID = 'workbench.picker.editors.one';
protected getPosition(): Position {
return Position.ONE;
}
@@ -227,6 +224,8 @@ export class GroupOnePicker extends EditorGroupPicker {
export class GroupTwoPicker extends EditorGroupPicker {
public static readonly ID = 'workbench.picker.editors.two';
protected getPosition(): Position {
return Position.TWO;
}
@@ -234,6 +233,8 @@ export class GroupTwoPicker extends EditorGroupPicker {
export class GroupThreePicker extends EditorGroupPicker {
public static readonly ID = 'workbench.picker.editors.three';
protected getPosition(): Position {
return Position.THREE;
}
@@ -241,6 +242,8 @@ export class GroupThreePicker extends EditorGroupPicker {
export class AllEditorsPicker extends BaseEditorPicker {
public static readonly ID = 'workbench.picker.editors';
protected getEditorEntries(): EditorPickerEntry[] {
const entries: EditorPickerEntry[] = [];

View File

@@ -8,7 +8,7 @@
import 'vs/css!./media/editorstatus';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import { $, append, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { $, append, runAtThisOrScheduleAtNextAnimationFrame, addDisposableListener } from 'vs/base/browser/dom';
import strings = require('vs/base/common/strings');
import paths = require('vs/base/common/paths');
import types = require('vs/base/common/types');
@@ -17,12 +17,12 @@ import errors = require('vs/base/common/errors');
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { Action } from 'vs/base/common/actions';
import { language, LANGUAGE_DEFAULT, AccessibilitySupport } from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { IMode } from 'vs/editor/common/modes';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput } from 'vs/workbench/common/editor';
import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing';
import { IEditorAction, ICommonCodeEditor, EndOfLineSequence, IModel } from 'vs/editor/common/editorCommon';
import { IModelLanguageChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { TrimTrailingWhitespaceAction } from 'vs/editor/contrib/linesOperations/common/linesOperations';
@@ -33,7 +33,7 @@ import { IEditor as IBaseEditor, IEditorInput } from 'vs/platform/editor/common/
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { SUPPORTED_ENCODINGS, IFileService } from 'vs/platform/files/common/files';
import { SUPPORTED_ENCODINGS, IFileService, IFilesConfiguration, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
@@ -43,13 +43,19 @@ import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { getCodeEditor as getEditorWidget } from 'vs/editor/common/services/codeEditorService';
import { getCodeEditor as getEditorWidget, getCodeOrDiffEditor } from 'vs/editor/common/services/codeEditorService';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
import { IConfigurationChangedEvent, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
import { widgetShadow, editorWidgetBackground } from 'vs/platform/theme/common/colorRegistry';
// TODO@Sandeep layer breaker
// tslint:disable-next-line:import-patterns
import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'
// {{SQL CARBON EDIT}}
import { QueryEditorService } from 'sql/parts/query/services/queryEditorService';
@@ -233,7 +239,7 @@ const nlsMultiSelection = nls.localize('multiSelection', "{0} selections");
const nlsEOLLF = nls.localize('endOfLineLineFeed', "LF");
const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF");
const nlsTabFocusMode = nls.localize('tabFocusModeEnabled', "Tab Moves Focus");
const nlsScreenReaderDetected = nls.localize('screenReaderDetected', "Screen Reader Detected");
const nlsScreenReaderDetected = nls.localize('screenReaderDetected', "Screen Reader Optimized");
const nlsScreenReaderDetectedTitle = nls.localize('screenReaderDetectedExtra', "If you are not using a Screen Reader, please change the setting `editor.accessibilitySupport` to \"off\".");
function _setDisplay(el: HTMLElement, desiredValue: string): void {
@@ -264,6 +270,7 @@ export class EditorStatus implements IStatusbarItem {
private activeEditorListeners: IDisposable[];
private delayedRender: IDisposable;
private toRender: StateChange;
private lastScreenReaderExplanation: ScreenReaderDetectedExplanation;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@@ -272,11 +279,13 @@ export class EditorStatus implements IStatusbarItem {
@IInstantiationService private instantiationService: IInstantiationService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IModeService private modeService: IModeService,
@ITextFileService private textFileService: ITextFileService
@ITextFileService private textFileService: ITextFileService,
@IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService,
) {
this.toDispose = [];
this.activeEditorListeners = [];
this.state = new State();
this.lastScreenReaderExplanation = null;
}
public render(container: HTMLElement): IDisposable {
@@ -291,6 +300,7 @@ export class EditorStatus implements IStatusbarItem {
this.screenRedearModeElement = append(this.element, $('a.editor-status-screenreadermode.status-bar-info'));
this.screenRedearModeElement.textContent = nlsScreenReaderDetected;
this.screenRedearModeElement.title = nlsScreenReaderDetectedTitle;
this.screenRedearModeElement.onclick = () => this.onScreenReaderModeClick();
hide(this.screenRedearModeElement);
this.selectionElement = append(this.element, $('a.editor-status-selection'));
@@ -472,6 +482,10 @@ export class EditorStatus implements IStatusbarItem {
action.dispose();
}
private onScreenReaderModeClick(): void {
this.lastScreenReaderExplanation = this.instantiationService.createInstance(ScreenReaderDetectedExplanation, this.screenRedearModeElement);
}
private onSelectionClick(): void {
this.quickOpenService.show(':'); // "Go to line"
}
@@ -610,15 +624,35 @@ export class EditorStatus implements IStatusbarItem {
this.updateState(update);
}
private _promptedScreenReader: boolean = false;
private onScreenReaderModeChange(editorWidget: ICommonCodeEditor): void {
let screenReaderMode = false;
// We only support text based editors
if (editorWidget) {
const screenReaderDetected = (browser.getAccessibilitySupport() === AccessibilitySupport.Enabled);
if (screenReaderDetected) {
const screenReaderConfiguration = this.configurationService.getConfiguration<IEditorOptions>('editor').accessibilitySupport;
if (screenReaderConfiguration === 'auto') {
// show explanation
if (!this._promptedScreenReader) {
this._promptedScreenReader = true;
setTimeout(() => {
this.onScreenReaderModeClick();
}, 100);
}
}
}
screenReaderMode = (editorWidget.getConfiguration().accessibilitySupport === AccessibilitySupport.Enabled);
}
if (screenReaderMode === false && this.lastScreenReaderExplanation) {
this.lastScreenReaderExplanation.hide();
this.lastScreenReaderExplanation = null;
}
this.updateState({ screenReaderMode: screenReaderMode });
}
@@ -699,7 +733,7 @@ export class EditorStatus implements IStatusbarItem {
private onResourceEncodingChange(resource: uri): void {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor) {
const activeResource = toResource(activeEditor.input, { supportSideBySide: true, filter: ['file', 'untitled'] });
const activeResource = toResource(activeEditor.input, { supportSideBySide: true });
if (activeResource && activeResource.toString() === resource.toString()) {
return this.onEncodingChange(<IBaseEditor>activeEditor); // only update if the encoding changed for the active resource
}
@@ -755,21 +789,18 @@ export class ChangeModeAction extends Action {
public static ID = 'workbench.action.editor.changeLanguageMode';
public static LABEL = nls.localize('changeMode', "Change Language Mode");
private static FILE_ASSOCIATION_KEY = 'files.associations';
constructor(
actionId: string,
actionLabel: string,
@IModeService private modeService: IModeService,
@IModelService private modelService: IModelService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IConfigurationEditingService private configurationEditingService: IConfigurationEditingService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@IQuickOpenService private quickOpenService: IQuickOpenService,
@IPreferencesService private preferencesService: IPreferencesService,
@IInstantiationService private instantiationService: IInstantiationService,
@ICommandService private commandService: ICommandService,
@IConfigurationEditingService private configurationEditService: IConfigurationEditingService
@IUntitledEditorService private untitledEditorService: IUntitledEditorService
) {
super(actionId, actionLabel);
}
@@ -782,11 +813,16 @@ export class ChangeModeAction extends Action {
}
const textModel = editorWidget.getModel();
const fileResource = toResource(activeEditor.input, { supportSideBySide: true, filter: 'file' });
const resource = toResource(activeEditor.input, { supportSideBySide: true });
let hasLanguageSupport = !!resource;
if (resource.scheme === 'untitled' && !this.untitledEditorService.hasAssociatedFilePath(resource)) {
hasLanguageSupport = false; // no configuration for untitled resources (e.g. "Untitled-1")
}
// Compute mode
let currentModeId: string;
let modeId;
let modeId: string;
if (textModel) {
modeId = textModel.getLanguageIdentifier().language;
currentModeId = this.modeService.getLanguageName(modeId);
@@ -821,7 +857,7 @@ export class ChangeModeAction extends Action {
};
});
if (fileResource) {
if (hasLanguageSupport) {
picks[0].separator = { border: true, label: nls.localize('languagesPicks', "languages (identifier)") };
}
@@ -829,15 +865,14 @@ export class ChangeModeAction extends Action {
let configureModeAssociations: IPickOpenEntry;
let configureModeSettings: IPickOpenEntry;
let galleryAction: Action;
if (fileResource) {
const ext = paths.extname(fileResource.fsPath) || paths.basename(fileResource.fsPath);
if (hasLanguageSupport) {
const ext = paths.extname(resource.fsPath) || paths.basename(resource.fsPath);
galleryAction = this.instantiationService.createInstance(ShowLanguageExtensionsAction, ext);
if (galleryAction.enabled) {
picks.unshift(galleryAction);
}
configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentModeId) };
picks.unshift(configureModeSettings);
configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) };
@@ -848,7 +883,8 @@ export class ChangeModeAction extends Action {
const autoDetectMode: IPickOpenEntry = {
label: nls.localize('autoDetect', "Auto Detect")
};
if (fileResource) {
if (hasLanguageSupport) {
picks.unshift(autoDetectMode);
}
@@ -864,7 +900,7 @@ export class ChangeModeAction extends Action {
// User decided to permanently configure associations, return right after
if (pick === configureModeAssociations) {
this.configureFileAssociation(fileResource);
this.configureFileAssociation(resource);
return;
}
@@ -876,36 +912,46 @@ export class ChangeModeAction extends Action {
// Change mode for active editor
activeEditor = this.editorService.getActiveEditor();
const editorWidget = getEditorWidget(activeEditor);
if (editorWidget) {
const models: IModel[] = [];
const textModel = editorWidget.getModel();
if (textModel) {
models.push(textModel);
const codeOrDiffEditor = getCodeOrDiffEditor(activeEditor);
const models: IModel[] = [];
if (codeOrDiffEditor.codeEditor) {
const codeEditorModel = codeOrDiffEditor.codeEditor.getModel();
if (codeEditorModel) {
models.push(codeEditorModel);
}
// Find mode
let mode: TPromise<IMode>;
if (pick === autoDetectMode) {
mode = this.modeService.getOrCreateModeByFilenameOrFirstLine(toResource(activeEditor.input, { supportSideBySide: true, filter: ['file', 'untitled'] }).fsPath, textModel.getLineContent(1));
} else {
mode = this.modeService.getOrCreateModeByLanguageName(pick.label);
}
if (codeOrDiffEditor.diffEditor) {
const diffEditorModel = codeOrDiffEditor.diffEditor.getModel();
if (diffEditorModel) {
if (diffEditorModel.original) {
models.push(diffEditorModel.original);
}
if (diffEditorModel.modified) {
models.push(diffEditorModel.modified);
}
}
}
// {{SQL CARBON EDIT}}
// Change mode
models.forEach(textModel => {
let self = this;
mode.then((modeValue) => {
QueryEditorService.sqlLanguageModeCheck(textModel, modeValue, activeEditor).then((newTextModel) => {
if (newTextModel) {
self.modelService.setMode(newTextModel, modeValue);
}
});
// Find mode
let mode: TPromise<IMode>;
if (pick === autoDetectMode) {
mode = this.modeService.getOrCreateModeByFilenameOrFirstLine(toResource(activeEditor.input, { supportSideBySide: true }).fsPath, textModel.getLineContent(1));
} else {
mode = this.modeService.getOrCreateModeByLanguageName(pick.label);
}
// {{SQL CARBON EDIT}}
// Change mode
models.forEach(textModel => {
let self = this;
mode.then((modeValue) => {
QueryEditorService.sqlLanguageModeCheck(textModel, modeValue, activeEditor).then((newTextModel) => {
if (newTextModel) {
self.modelService.setMode(newTextModel, modeValue);
}
});
});
}
});
});
}
@@ -928,7 +974,7 @@ export class ChangeModeAction extends Action {
TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).done(() => {
this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || basename) }).done(language => {
if (language) {
const fileAssociationsConfig = this.configurationService.lookup(ChangeModeAction.FILE_ASSOCIATION_KEY);
const fileAssociationsConfig = this.configurationService.inspect(FILES_ASSOCIATIONS_CONFIG);
let associationKey: string;
if (extension && basename[0] !== '.') {
@@ -951,8 +997,7 @@ export class ChangeModeAction extends Action {
currentAssociations[associationKey] = language.id;
// Write config
this.configurationEditingService.writeConfiguration(target, { key: ChangeModeAction.FILE_ASSOCIATION_KEY, value: currentAssociations });
this.configurationService.updateValue(FILES_ASSOCIATIONS_CONFIG, currentAssociations, target);
}
});
});
@@ -1070,7 +1115,7 @@ export class ChangeEncodingAction extends Action {
actionLabel: string,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IQuickOpenService private quickOpenService: IQuickOpenService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
@IFileService private fileService: IFileService
) {
super(actionId, actionLabel);
@@ -1112,19 +1157,22 @@ export class ChangeEncodingAction extends Action {
return void 0;
}
const resource = toResource(activeEditor.input, { filter: ['file', 'untitled'], supportSideBySide: true });
const resource = toResource(activeEditor.input, { supportSideBySide: true });
return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */)
.then(() => {
if (!resource || resource.scheme !== 'file') {
return TPromise.as(null); // encoding detection only possible for file resources
if (!resource || !this.fileService.canHandleResource(resource)) {
return TPromise.as(null); // encoding detection only possible for resources the file service can handle
}
return this.fileService.resolveContent(resource, { autoGuessEncoding: true, acceptTextOnly: true }).then(content => content.encoding, err => null);
})
.then((guessedEncoding: string) => {
const isReopenWithEncoding = (action === reopenWithEncodingPick);
const configuredEncoding = this.configurationService.lookup('files.encoding', { resource }).value;
const config = this.textResourceConfigurationService.getConfiguration(resource) as IFilesConfiguration;
const configuredEncoding = config && config.files && config.files.encoding;
let directMatchIndex: number;
let aliasMatchIndex: number;
@@ -1153,7 +1201,7 @@ export class ChangeEncodingAction extends Action {
aliasMatchIndex = index;
}
return { id: key, label: SUPPORTED_ENCODINGS[key].labelLong };
return { id: key, label: SUPPORTED_ENCODINGS[key].labelLong, description: key };
});
// If we have a guessed encoding, show it first unless it matches the configured encoding
@@ -1178,3 +1226,107 @@ export class ChangeEncodingAction extends Action {
});
}
}
class ScreenReaderDetectedExplanation {
private _isDisposed: boolean;
private _toDispose: IDisposable[];
constructor(
anchorElement: HTMLElement,
@IThemeService private readonly themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService,
) {
this._isDisposed = false;
this._toDispose = [];
this.contextViewService.showContextView({
getAnchor: () => anchorElement,
render: (container) => {
return this.renderContents(container);
},
onDOMEvent: (e, activeElement) => {
},
onHide: () => {
this.dispose();
}
});
}
public dispose(): void {
this._isDisposed = true;
this._toDispose = dispose(this._toDispose);
}
public hide(): void {
if (this._isDisposed) {
return;
}
this.contextViewService.hideContextView();
}
protected renderContents(container: HTMLElement): IDisposable {
const domNode = $('div.screen-reader-detected-explanation', {
'aria-hidden': 'true'
});
const title = $('h2.title', {}, nls.localize('screenReaderDetectedExplanation.title', "Screen Reader Optimized"));
domNode.appendChild(title);
const closeBtn = $('div.cancel');
this._toDispose.push(addDisposableListener(closeBtn, 'click', () => {
this.contextViewService.hideContextView();
}));
domNode.appendChild(closeBtn);
const question = $('p.question', {}, nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code?"));
domNode.appendChild(question);
const yesBtn = $('div.button', {}, nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"));
this._toDispose.push(addDisposableListener(yesBtn, 'click', () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'on', ConfigurationTarget.USER);
this.contextViewService.hideContextView();
}));
domNode.appendChild(yesBtn);
const noBtn = $('div.button', {}, nls.localize('screenReaderDetectedExplanation.answerNo', "No"));
this._toDispose.push(addDisposableListener(noBtn, 'click', () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'off', ConfigurationTarget.USER);
this.contextViewService.hideContextView();
}));
domNode.appendChild(noBtn);
const clear = $('div');
clear.style.clear = 'both';
domNode.appendChild(clear);
const br = $('br');
domNode.appendChild(br);
const hr = $('hr');
domNode.appendChild(hr);
const explanation1 = $('p.body1', {}, nls.localize('screenReaderDetectedExplanation.body1', "VS Code is now optimized for usage with a screen reader."));
domNode.appendChild(explanation1);
const explanation2 = $('p.body2', {}, nls.localize('screenReaderDetectedExplanation.body2', "Some editor features will have different behaviour: e.g. word wrapping, folding, etc."));
domNode.appendChild(explanation2);
container.appendChild(domNode);
this._toDispose.push(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground }, colors => {
domNode.style.backgroundColor = colors.editorWidgetBackground;
if (colors.widgetShadow) {
domNode.style.boxShadow = `0 2px 8px ${colors.widgetShadow}`;
}
}));
return {
dispose: () => { this.dispose(); }
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#C5C5C5" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -18,7 +18,78 @@
padding: 0 5px 0 5px;
}
.monaco-workbench .editor-statusbar-item > .editor-status-metadata,
.monaco-workbench > .part.statusbar > .statusbar-item > .editor-statusbar-item > a.editor-status-screenreadermode {
.monaco-workbench .editor-statusbar-item > .editor-status-metadata {
cursor: default !important;
}
.monaco-shell .screen-reader-detected-explanation {
width: 420px;
top: 30px;
right: 6px;
padding: 1em;
cursor: default;
}
.monaco-shell .screen-reader-detected-explanation .cancel {
position: absolute;
top: 0;
right: 0;
margin: .5em 0 0;
padding: .5em;
width: 22px;
height: 22px;
border: none;
cursor: pointer;
}
.monaco-shell .screen-reader-detected-explanation h2 {
margin: 0;
padding: 0;
font-weight: 400;
font-size: 1.8em;
}
.monaco-shell .screen-reader-detected-explanation p {
font-size: 1.2em;
}
.monaco-shell .screen-reader-detected-explanation p.question {
font-size: 1.4em;
font-weight: bold;
}
.monaco-shell .screen-reader-detected-explanation .button {
color: white;
border: none;
cursor: pointer;
background-color: #007ACC;
padding-left: 12px;
padding-right: 12px;
border: 4px solid #007ACC;
border-radius: 4px;
float: left;
margin-right: 5px;
}
.monaco-shell.vs .screen-reader-detected-explanation .cancel {
background: url('close-big.svg') center center no-repeat;
}
.monaco-shell.vs .screen-reader-detected-explanation .cancel:hover {
background-color: #eaeaea;
}
.monaco-shell.vs-dark .screen-reader-detected-explanation .cancel,
.monaco-shell.hc-black .screen-reader-detected-explanation .cancel {
background: url('close-big-dark.svg') center center no-repeat;
}
.monaco-shell.vs-dark .screen-reader-detected-explanation .cancel:hover {
background-color: rgba(30,30,30,0.8);
}
.monaco-shell.hc-black .screen-reader-detected-explanation .cancel {
opacity: 0.6;
}
.monaco-shell.hc-black .screen-reader-detected-explanation .cancel:hover {
opacity: 1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -10,13 +10,15 @@ import errors = require('vs/base/common/errors');
import { IEditorGroup, toResource } from 'vs/workbench/common/editor';
import DOM = require('vs/base/browser/dom');
import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl';
import { EditorLabel } from 'vs/workbench/browser/labels';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { Verbosity } from 'vs/platform/editor/common/editor';
import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme';
import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
export class NoTabsTitleControl extends TitleControl {
private titleContainer: HTMLElement;
private editorLabel: EditorLabel;
private editorLabel: ResourceLabel;
private titleTouchSupport: Gesture;
public setContext(group: IEditorGroup): void {
super.setContext(group);
@@ -29,17 +31,22 @@ export class NoTabsTitleControl extends TitleControl {
this.titleContainer = parent;
// Gesture Support
this.titleTouchSupport = new Gesture(this.titleContainer);
// Pin on double click
this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, DOM.EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e)));
// Detect mouse click
this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, DOM.EventType.CLICK, (e: MouseEvent) => this.onTitleClick(e)));
// Detect touch
this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleClick(e)));
// Editor Label
this.editorLabel = this.instantiationService.createInstance(EditorLabel, this.titleContainer, void 0);
this.editorLabel = this.instantiationService.createInstance(ResourceLabel, this.titleContainer, void 0);
this.toUnbind.push(this.editorLabel);
this.toUnbind.push(DOM.addDisposableListener(this.editorLabel.labelElement, DOM.EventType.CLICK, (e: MouseEvent) => this.onTitleLabelClick(e)));
this.toUnbind.push(DOM.addDisposableListener(this.editorLabel.descriptionElement, DOM.EventType.CLICK, (e: MouseEvent) => this.onTitleLabelClick(e)));
this.toUnbind.push(this.editorLabel.onClick(e => this.onTitleLabelClick(e)));
// Right Actions Container
const actionsContainer = document.createElement('div');
@@ -51,6 +58,7 @@ export class NoTabsTitleControl extends TitleControl {
// Context Menu
this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, DOM.EventType.CONTEXT_MENU, (e: Event) => this.onContextMenu({ group: this.context, editor: this.context.activeEditor }, e, this.titleContainer)));
this.toUnbind.push(DOM.addDisposableListener(this.titleContainer, TouchEventType.Contextmenu, (e: Event) => this.onContextMenu({ group: this.context, editor: this.context.activeEditor }, e, this.titleContainer)));
}
private onTitleLabelClick(e: MouseEvent): void {
@@ -71,7 +79,7 @@ export class NoTabsTitleControl extends TitleControl {
this.editorGroupService.pinEditor(group, group.activeEditor);
}
private onTitleClick(e: MouseEvent): void {
private onTitleClick(e: MouseEvent | GestureEvent): void {
if (!this.context) {
return;
}
@@ -79,7 +87,7 @@ export class NoTabsTitleControl extends TitleControl {
const group = this.context;
// Close editor on middle mouse click
if (e.button === 1 /* Middle Button */) {
if (e instanceof MouseEvent && e.button === 1 /* Middle Button */) {
this.closeEditorAction.run({ group, editor: group.activeEditor }).done(null, errors.onUnexpectedError);
}
@@ -119,7 +127,15 @@ export class NoTabsTitleControl extends TitleControl {
// Editor Label
const resource = toResource(editor, { supportSideBySide: true });
const name = editor.getName() || '';
const description = isActive ? (editor.getDescription() || '') : '';
const labelFormat = this.editorGroupService.getTabOptions().labelFormat;
let description: string;
if (labelFormat === 'default' && !isActive) {
description = ''; // hide description when group is not active and style is 'default'
} else {
description = editor.getDescription(this.getVerbosity(labelFormat)) || '';
}
let title = editor.getTitle(Verbosity.LONG);
if (description === title) {
title = ''; // dont repeat what is already shown
@@ -135,4 +151,18 @@ export class NoTabsTitleControl extends TitleControl {
// Update Editor Actions Toolbar
this.updateEditorActionsToolbar();
}
private getVerbosity(style: string): Verbosity {
switch (style) {
case 'short': return Verbosity.SHORT;
case 'long': return Verbosity.LONG;
default: return Verbosity.MEDIUM;
}
}
public dispose(): void {
super.dispose();
this.titleTouchSupport.dispose();
}
}

View File

@@ -4,13 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import * as strings from 'vs/base/common/strings';
import * as DOM from 'vs/base/browser/dom';
import { Dimension, Builder } from 'vs/base/browser/builder';
import { Registry } from 'vs/platform/registry/common/platform';
import { IEditorRegistry, Extensions as EditorExtensions, EditorInput, EditorOptions, SideBySideEditorInput } from 'vs/workbench/common/editor';
import { BaseEditor, EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, SideBySideEditorInput } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IEditorControl, Position, IEditor } from 'vs/platform/editor/common/editor';
import { VSash } from 'vs/base/browser/ui/sash/sash';
@@ -18,6 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
export class SideBySideEditor extends BaseEditor {
@@ -110,7 +110,7 @@ export class SideBySideEditor extends BaseEditor {
return this.detailsEditor;
}
private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options?: EditorOptions): TPromise<void> {
private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options?: EditorOptions): void {
if (!newInput.matches(oldInput)) {
if (oldInput) {
this.disposeEditors();
@@ -126,24 +126,21 @@ export class SideBySideEditor extends BaseEditor {
}
}
private setNewInput(newInput: SideBySideEditorInput, options?: EditorOptions): TPromise<void> {
return TPromise.join([
this._createEditor(<EditorInput>newInput.details, this.detailsEditorContainer),
this._createEditor(<EditorInput>newInput.master, this.masterEditorContainer)
]).then(result => this.onEditorsCreated(result[0], result[1], newInput.details, newInput.master, options));
private setNewInput(newInput: SideBySideEditorInput, options?: EditorOptions): void {
const detailsEditor = this._createEditor(<EditorInput>newInput.details, this.detailsEditorContainer);
const masterEditor = this._createEditor(<EditorInput>newInput.master, this.masterEditorContainer);
this.onEditorsCreated(detailsEditor, masterEditor, newInput.details, newInput.master, options);
}
private _createEditor(editorInput: EditorInput, container: HTMLElement): TPromise<BaseEditor> {
private _createEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor {
const descriptor = Registry.as<IEditorRegistry>(EditorExtensions.Editors).getEditor(editorInput);
if (!descriptor) {
return TPromise.wrapError<BaseEditor>(new Error(strings.format('Can not find a registered editor for the input {0}', editorInput)));
}
return this.instantiationService.createInstance(<EditorDescriptor>descriptor)
.then((editor: BaseEditor) => {
editor.create(new Builder(container));
editor.setVisible(this.isVisible(), this.position);
return editor;
});
const editor = descriptor.instantiate(this.instantiationService);
editor.create(new Builder(container));
editor.setVisible(this.isVisible(), this.position);
return editor;
}
private onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions): TPromise<void> {

View File

@@ -17,10 +17,11 @@ import { ActionRunner, IAction } from 'vs/base/common/actions';
import { Position, IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
import { IEditorGroup, toResource } from 'vs/workbench/common/editor';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
import { KeyCode } from 'vs/base/common/keyCodes';
import { EditorLabel } from 'vs/workbench/browser/labels';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEditorService, DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IMessageService } from 'vs/platform/message/common/message';
@@ -37,7 +38,6 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { extractResources } from 'vs/base/browser/dnd';
import { getOrSet } from 'vs/base/common/map';
import { DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/browser/editorService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } 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_BORDER, TAB_ACTIVE_BORDER } from 'vs/workbench/common/theme';
@@ -47,16 +47,17 @@ import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
interface IEditorInputLabel {
name: string;
hasAmbiguousName?: boolean;
description?: string;
title?: string;
}
type AugmentedLabel = IEditorInputLabel & { editor: IEditorInput };
export class TabsTitleControl extends TitleControl {
private titleContainer: HTMLElement;
private tabsContainer: HTMLElement;
private activeTab: HTMLElement;
private editorLabels: EditorLabel[];
private editorLabels: ResourceLabel[];
private scrollbar: ScrollableElement;
private tabDisposeables: IDisposable[];
private blockRevealActiveTab: boolean;
@@ -264,7 +265,7 @@ export class TabsTitleControl extends TitleControl {
// Compute labels and protect against duplicates
const editorsOfGroup = this.context.getEditors();
const labels = this.getUniqueTabLabels(editorsOfGroup);
const labels = this.getTabLabels(editorsOfGroup);
// Tab label and styles
editorsOfGroup.forEach((editor, index) => {
@@ -276,7 +277,7 @@ export class TabsTitleControl extends TitleControl {
const label = labels[index];
const name = label.name;
const description = label.hasAmbiguousName && label.description ? label.description : '';
const description = label.description || '';
const title = label.title || '';
// Container
@@ -294,7 +295,8 @@ export class TabsTitleControl extends TitleControl {
// Label
const tabLabel = this.editorLabels[index];
tabLabel.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !isPinned });
// {{SQL CARBON EDIT}} -- add title in options passed
tabLabel.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !isPinned, title });
// Active state
if (isTabActive) {
@@ -338,56 +340,108 @@ export class TabsTitleControl extends TitleControl {
this.layout();
}
private getUniqueTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
const labels: IEditorInputLabel[] = [];
const mapLabelToDuplicates = new Map<string, IEditorInputLabel[]>();
const mapLabelAndDescriptionToDuplicates = new Map<string, IEditorInputLabel[]>();
private getTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
const labelFormat = this.editorGroupService.getTabOptions().labelFormat;
const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat);
// Build labels and descriptions for each editor
editors.forEach(editor => {
const name = editor.getName();
let description = editor.getDescription();
if (mapLabelAndDescriptionToDuplicates.has(`${name}${description}`)) {
description = editor.getDescription(true); // try verbose description if name+description already exists
}
const labels = editors.map(editor => ({
editor,
name: editor.getName(),
description: editor.getDescription(verbosity),
title: editor.getTitle(Verbosity.LONG)
}));
const item: IEditorInputLabel = {
name,
description,
title: editor.getTitle(Verbosity.LONG)
};
labels.push(item);
getOrSet(mapLabelToDuplicates, item.name, []).push(item);
if (typeof description === 'string') {
getOrSet(mapLabelAndDescriptionToDuplicates, `${item.name}${item.description}`, []).push(item);
}
});
// Mark duplicates and shorten their descriptions
mapLabelToDuplicates.forEach(duplicates => {
if (duplicates.length > 1) {
duplicates = duplicates.filter(d => {
// we could have items with equal label and description. in that case it does not make much
// sense to produce a shortened version of the label, so we ignore those kind of items
return typeof d.description === 'string' && mapLabelAndDescriptionToDuplicates.get(`${d.name}${d.description}`).length === 1;
});
if (duplicates.length > 1) {
const shortenedDescriptions = shorten(duplicates.map(duplicate => duplicate.description));
duplicates.forEach((duplicate, i) => {
duplicate.description = shortenedDescriptions[i];
duplicate.hasAmbiguousName = true;
});
}
}
});
// Shorten labels as needed
if (shortenDuplicates) {
this.shortenTabLabels(labels);
}
return labels;
}
private shortenTabLabels(labels: AugmentedLabel[]): void {
// Gather duplicate titles, while filtering out invalid descriptions
const mapTitleToDuplicates = new Map<string, AugmentedLabel[]>();
for (const label of labels) {
if (typeof label.description === 'string') {
getOrSet(mapTitleToDuplicates, label.name, []).push(label);
} else {
label.description = '';
}
}
// Identify duplicate titles and shorten descriptions
mapTitleToDuplicates.forEach(duplicateTitles => {
// Remove description if the title isn't duplicated
if (duplicateTitles.length === 1) {
duplicateTitles[0].description = '';
return;
}
// Identify duplicate descriptions
const mapDescriptionToDuplicates = new Map<string, AugmentedLabel[]>();
for (const label of duplicateTitles) {
getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
}
// For editors with duplicate descriptions, check whether any long descriptions differ
let useLongDescriptions = false;
mapDescriptionToDuplicates.forEach((duplicateDescriptions, name) => {
if (!useLongDescriptions && duplicateDescriptions.length > 1) {
const [first, ...rest] = duplicateDescriptions.map(({ editor }) => editor.getDescription(Verbosity.LONG));
useLongDescriptions = rest.some(description => description !== first);
}
});
// If so, replace all descriptions with long descriptions
if (useLongDescriptions) {
mapDescriptionToDuplicates.clear();
duplicateTitles.forEach(label => {
label.description = label.editor.getDescription(Verbosity.LONG);
getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
});
}
// Obtain final set of descriptions
const descriptions: string[] = [];
mapDescriptionToDuplicates.forEach((_, description) => descriptions.push(description));
// Remove description if all descriptions are identical
if (descriptions.length === 1) {
for (const label of mapDescriptionToDuplicates.get(descriptions[0])) {
label.description = '';
}
return;
}
// Shorten descriptions
const shortenedDescriptions = shorten(descriptions);
descriptions.forEach((description, i) => {
for (const label of mapDescriptionToDuplicates.get(description)) {
label.description = shortenedDescriptions[i];
}
});
});
}
private getLabelConfigFlags(value: string) {
switch (value) {
case 'short':
return { verbosity: Verbosity.SHORT, shortenDuplicates: false };
case 'medium':
return { verbosity: Verbosity.MEDIUM, shortenDuplicates: false };
case 'long':
return { verbosity: Verbosity.LONG, shortenDuplicates: false };
default:
return { verbosity: Verbosity.MEDIUM, shortenDuplicates: true };
}
}
protected doRefresh(): void {
const group = this.context;
const editor = group && group.activeEditor;
@@ -451,8 +505,11 @@ export class TabsTitleControl extends TitleControl {
tabContainer.setAttribute('role', 'presentation'); // cannot use role "tab" here due to https://github.com/Microsoft/vscode/issues/8659
DOM.addClass(tabContainer, 'tab');
// Gesture Support
const gestureSupport = new Gesture(tabContainer);
// Tab Editor Label
const editorLabel = this.instantiationService.createInstance(EditorLabel, tabContainer, void 0);
const editorLabel = this.instantiationService.createInstance(ResourceLabel, tabContainer, void 0);
this.editorLabels.push(editorLabel);
// Tab Close
@@ -466,7 +523,7 @@ export class TabsTitleControl extends TitleControl {
// Eventing
const disposable = this.hookTabListeners(tabContainer, index);
this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel]));
this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel, gestureSupport]));
return tabContainer;
}
@@ -516,14 +573,42 @@ export class TabsTitleControl extends TitleControl {
private hookTabListeners(tab: HTMLElement, index: number): IDisposable {
const disposables: IDisposable[] = [];
// Open on Click
disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
const handleClickOrTouch = (e: MouseEvent | GestureEvent) => {
tab.blur();
if (e instanceof MouseEvent && e.button !== 0) {
if (e.button === 1) {
return false; // required due to https://github.com/Microsoft/vscode/issues/16690
}
return void 0; // only for left mouse click
}
const { editor, position } = this.toTabContext(index);
if (e.button === 0 /* Left Button */ && !this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) {
if (!this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) {
setTimeout(() => this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError)); // timeout to keep focus in editor after mouse up
}
return void 0;
};
const showContextMenu = (e: Event) => {
DOM.EventHelper.stop(e);
const { group, editor } = this.toTabContext(index);
this.onContextMenu({ group, editor }, e, tab);
};
// Open on Click
disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => handleClickOrTouch(e)));
// Open on Touch
disposables.push(DOM.addDisposableListener(tab, TouchEventType.Tap, (e: GestureEvent) => handleClickOrTouch(e)));
// Touch Scroll Support
disposables.push(DOM.addDisposableListener(tab, TouchEventType.Change, (e: GestureEvent) => {
this.tabsContainer.scrollLeft -= e.translationX;
}));
// Close on mouse middle click
@@ -540,14 +625,15 @@ export class TabsTitleControl extends TitleControl {
disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.shiftKey && event.keyCode === KeyCode.F10) {
DOM.EventHelper.stop(e);
const { group, editor } = this.toTabContext(index);
this.onContextMenu({ group, editor }, e, tab);
showContextMenu(e);
}
}));
// Context menu on touch context menu gesture
disposables.push(DOM.addDisposableListener(tab, TouchEventType.Contextmenu, (e: GestureEvent) => {
showContextMenu(e);
}));
// Keyboard accessibility
disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
@@ -617,12 +703,15 @@ export class TabsTitleControl extends TitleControl {
e.dataTransfer.effectAllowed = 'copyMove';
// Insert transfer accordingly
const fileResource = toResource(editor, { supportSideBySide: true, filter: 'file' });
if (fileResource) {
const resource = fileResource.toString();
e.dataTransfer.setData('URL', resource); // enables cross window DND of tabs
e.dataTransfer.setData('DownloadURL', [MIME_BINARY, editor.getName(), resource].join(':')); // enables support to drag a tab as file to desktop
const resource = toResource(editor, { supportSideBySide: true });
if (resource) {
const resourceStr = resource.toString();
e.dataTransfer.setData('URL', resourceStr); // enables cross window DND of tabs
e.dataTransfer.setData('text/plain', getPathLabel(resource)); // enables dropping tab resource path into text controls
if (resource.scheme === 'file') {
e.dataTransfer.setData('DownloadURL', [MIME_BINARY, editor.getName(), resourceStr].join(':')); // enables support to drag a tab as file to desktop
}
}
}));
@@ -635,7 +724,21 @@ export class TabsTitleControl extends TitleControl {
// Drag over
disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
counter++;
this.updateDropFeedback(tab, true, index);
// Find out if the currently dragged editor is this tab and in that
// case we do not want to show any drop feedback
let draggedEditorIsTab = false;
const draggedEditor = TabsTitleControl.getDraggedEditor();
if (draggedEditor) {
const { group, editor } = this.toTabContext(index);
if (draggedEditor.editor === editor && draggedEditor.group === group) {
draggedEditorIsTab = true;
}
}
if (!draggedEditorIsTab) {
this.updateDropFeedback(tab, true, index);
}
}));
// Drag leave

View File

@@ -22,14 +22,13 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel';
import { DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/browser/editorService';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEditorService, DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IModeService } from 'vs/editor/common/services/modeService';
@@ -112,16 +111,12 @@ export class TextDiffEditor extends BaseTextEditor {
}
public setInput(input: EditorInput, options?: EditorOptions): TPromise<void> {
const oldInput = this.input;
super.setInput(input, options);
// Detect options
// Return early for same input unless we force to open
const forceOpen = options && options.forceOpen;
if (!forceOpen && input.matches(this.input)) {
// Same Input
if (!forceOpen && input.matches(oldInput)) {
// TextOptions (avoiding instanceof here for a reason, do not change!)
// Still apply options if any (avoiding instanceof here for a reason, do not change!)
const textOptions = <TextEditorOptions>options;
if (textOptions && types.isFunction(textOptions.apply)) {
textOptions.apply(<IDiffEditor>this.getControl(), ScrollType.Smooth);
@@ -135,49 +130,51 @@ export class TextDiffEditor extends BaseTextEditor {
this.diffNavigator.dispose();
}
// Different Input (Reload)
return input.resolve(true).then(resolvedModel => {
// Set input and resolve
return super.setInput(input, options).then(() => {
return input.resolve(true).then(resolvedModel => {
// Assert Model Instance
if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) {
return null;
}
// Assert that the current input is still the one we expect. This prevents a race condition when loading a diff takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Editor
const diffEditor = <IDiffEditor>this.getControl();
diffEditor.setModel((<TextDiffEditorModel>resolvedModel).textDiffEditorModel);
// Handle TextOptions
let alwaysRevealFirst = true;
if (options && types.isFunction((<TextEditorOptions>options).apply)) {
const hadOptions = (<TextEditorOptions>options).apply(<IDiffEditor>diffEditor, ScrollType.Immediate);
if (hadOptions) {
alwaysRevealFirst = false; // Do not reveal if we are instructed to open specific line/col
// Assert Model Instance
if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) {
return null;
}
}
// Listen on diff updated changes to reveal the first change
this.diffNavigator = new DiffNavigator(diffEditor, {
alwaysRevealFirst
// Assert that the current input is still the one we expect. This prevents a race condition when loading a diff takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Editor
const diffEditor = <IDiffEditor>this.getControl();
diffEditor.setModel((<TextDiffEditorModel>resolvedModel).textDiffEditorModel);
// Handle TextOptions
let alwaysRevealFirst = true;
if (options && types.isFunction((<TextEditorOptions>options).apply)) {
const hadOptions = (<TextEditorOptions>options).apply(<IDiffEditor>diffEditor, ScrollType.Immediate);
if (hadOptions) {
alwaysRevealFirst = false; // Do not reveal if we are instructed to open specific line/col
}
}
// Listen on diff updated changes to reveal the first change
this.diffNavigator = new DiffNavigator(diffEditor, {
alwaysRevealFirst
});
this.diffNavigator.addListener(DiffNavigator.Events.UPDATED, () => {
this.nextDiffAction.updateEnablement();
this.previousDiffAction.updateEnablement();
});
}, error => {
// In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff.
if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) {
return null;
}
// Otherwise make sure the error bubbles up
return TPromise.wrapError(error);
});
this.diffNavigator.addListener(DiffNavigator.Events.UPDATED, () => {
this.nextDiffAction.updateEnablement();
this.previousDiffAction.updateEnablement();
});
}, error => {
// In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff.
if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) {
return null;
}
// Otherwise make sure the error bubbles up
return TPromise.wrapError(error);
});
}

View File

@@ -14,7 +14,7 @@ import types = require('vs/base/common/types');
import errors = require('vs/base/common/errors');
import DOM = require('vs/base/browser/dom');
import { CodeEditor } from 'vs/editor/browser/codeEditor';
import { EditorInput, EditorOptions, toResource } from 'vs/workbench/common/editor';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IEditorViewState, IEditor, isCommonCodeEditor, isCommonDiffEditor } from 'vs/editor/common/editorCommon';
import { Position } from 'vs/platform/editor/common/editor';
@@ -66,7 +66,7 @@ export abstract class BaseTextEditor extends BaseEditor {
) {
super(id, telemetryService, themeService);
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getConfiguration<IEditorConfiguration>(this.getResource()))));
this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getConfiguration<IEditorConfiguration>(this.getResource()))));
}
protected get instantiationService(): IInstantiationService {
@@ -196,6 +196,7 @@ export abstract class BaseTextEditor extends BaseEditor {
// Update editor options after having set the input. We do this because there can be
// editor input specific options (e.g. an ARIA label depending on the input showing)
this.updateEditorConfiguration();
this._editorContainer.getHTMLElement().setAttribute('aria-label', this.computeAriaLabel());
});
}
@@ -240,7 +241,7 @@ export abstract class BaseTextEditor extends BaseEditor {
*/
protected saveTextEditorViewState(key: string): void {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
let textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
let textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
if (!textEditorViewStateMemento) {
textEditorViewStateMemento = Object.create(null);
memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY] = textEditorViewStateMemento;
@@ -248,7 +249,7 @@ export abstract class BaseTextEditor extends BaseEditor {
const editorViewState = this.getControl().saveViewState();
let lastKnownViewState: ITextEditorViewState = textEditorViewStateMemento[key];
let lastKnownViewState = textEditorViewStateMemento[key];
if (!lastKnownViewState) {
lastKnownViewState = Object.create(null);
textEditorViewStateMemento[key] = lastKnownViewState;
@@ -264,7 +265,7 @@ export abstract class BaseTextEditor extends BaseEditor {
*/
protected clearTextEditorViewState(keys: string[]): void {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
const textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
const textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
if (textEditorViewStateMemento) {
keys.forEach(key => delete textEditorViewStateMemento[key]);
}
@@ -275,9 +276,9 @@ export abstract class BaseTextEditor extends BaseEditor {
*/
protected loadTextEditorViewState(key: string): IEditorViewState {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
const textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
const textEditorViewStateMemento: { [key: string]: { [position: number]: IEditorViewState } } = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY];
if (textEditorViewStateMemento) {
const viewState: ITextEditorViewState = textEditorViewStateMemento[key];
const viewState = textEditorViewStateMemento[key];
if (viewState) {
return viewState[this.position];
}
@@ -317,7 +318,7 @@ export abstract class BaseTextEditor extends BaseEditor {
}
if (this.input) {
return toResource(this.input);
return this.input.getResource();
}
return null;

View File

@@ -55,16 +55,12 @@ export class TextResourceEditor extends BaseTextEditor {
}
public setInput(input: EditorInput, options?: EditorOptions): TPromise<void> {
const oldInput = this.input;
super.setInput(input, options);
// Detect options
// Return early for same input unless we force to open
const forceOpen = options && options.forceOpen;
if (!forceOpen && input.matches(this.input)) {
// Same Input
if (!forceOpen && input.matches(oldInput)) {
// TextOptions (avoiding instanceof here for a reason, do not change!)
// Still apply options if any (avoiding instanceof here for a reason, do not change!)
const textOptions = <TextEditorOptions>options;
if (textOptions && types.isFunction(textOptions.apply)) {
textOptions.apply(this.getControl(), ScrollType.Smooth);
@@ -74,39 +70,41 @@ export class TextResourceEditor extends BaseTextEditor {
}
// Remember view settings if input changes
this.saveTextEditorViewStateForInput(oldInput);
this.saveTextEditorViewStateForInput(this.input);
// Different Input (Reload)
return input.resolve(true).then((resolvedModel: EditorModel) => {
// Set input and resolve
return super.setInput(input, options).then(() => {
return input.resolve(true).then((resolvedModel: EditorModel) => {
// Assert Model instance
if (!(resolvedModel instanceof BaseTextEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as text'));
}
// Assert Model instance
if (!(resolvedModel instanceof BaseTextEditorModel)) {
return TPromise.wrapError<void>(new Error('Unable to open file as text'));
}
// Assert that the current input is still the one we expect. This prevents a race condition when loading takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Assert that the current input is still the one we expect. This prevents a race condition when loading takes long and another input was set meanwhile
if (!this.input || this.input !== input) {
return null;
}
// Set Editor Model
const textEditor = this.getControl();
const textEditorModel = resolvedModel.textEditorModel;
textEditor.setModel(textEditorModel);
// Set Editor Model
const textEditor = this.getControl();
const textEditorModel = resolvedModel.textEditorModel;
textEditor.setModel(textEditorModel);
// Apply Options from TextOptions
let optionsGotApplied = false;
const textOptions = <TextEditorOptions>options;
if (textOptions && types.isFunction(textOptions.apply)) {
optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate);
}
// Apply Options from TextOptions
let optionsGotApplied = false;
const textOptions = <TextEditorOptions>options;
if (textOptions && types.isFunction(textOptions.apply)) {
optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate);
}
// Otherwise restore View State
if (!optionsGotApplied) {
this.restoreViewState(input);
}
// Otherwise restore View State
if (!optionsGotApplied) {
this.restoreViewState(input);
}
return void 0;
return void 0;
});
});
}

View File

@@ -60,6 +60,7 @@ export interface ITitleAreaControl {
getContainer(): HTMLElement;
refresh(instant?: boolean): void;
update(instant?: boolean): void;
updateEditorActionsToolbar(): void;
layout(): void;
dispose(): void;
}
@@ -259,6 +260,12 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl
// Log in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'editorPart' });
}
}));
@@ -334,7 +341,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl
return { primary, secondary };
}
protected updateEditorActionsToolbar(): void {
public updateEditorActionsToolbar(): void {
const group = this.context;
if (!group) {
return;
@@ -543,7 +550,7 @@ export function handleWorkspaceExternalDrop(
// Multiple folders: Create new workspace with folders and open
else if (folders.length > 1) {
workspacesToOpen = workspacesService.createWorkspace([...folders].map(folder => folder.fsPath)).then(workspace => [workspace.configPath]);
workspacesToOpen = workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [workspace.configPath]);
}
// Open

View File

@@ -40,13 +40,13 @@ export abstract class BaseWebviewEditor extends BaseEditor {
protected saveViewState(resource: URI | string, editorViewState: HtmlPreviewEditorViewState): void {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
let editorViewStateMemento = memento[this.viewStateStorageKey];
let editorViewStateMemento: { [key: string]: { [position: number]: HtmlPreviewEditorViewState } } = memento[this.viewStateStorageKey];
if (!editorViewStateMemento) {
editorViewStateMemento = Object.create(null);
memento[this.viewStateStorageKey] = editorViewStateMemento;
}
let fileViewState: HtmlPreviewEditorViewStates = editorViewStateMemento[resource.toString()];
let fileViewState = editorViewStateMemento[resource.toString()];
if (!fileViewState) {
fileViewState = Object.create(null);
editorViewStateMemento[resource.toString()] = fileViewState;
@@ -59,9 +59,9 @@ export abstract class BaseWebviewEditor extends BaseEditor {
protected loadViewState(resource: URI | string): HtmlPreviewEditorViewState | null {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
const editorViewStateMemento = memento[this.viewStateStorageKey];
const editorViewStateMemento: { [key: string]: { [position: number]: HtmlPreviewEditorViewState } } = memento[this.viewStateStorageKey];
if (editorViewStateMemento) {
const fileViewState: HtmlPreviewEditorViewStates = editorViewStateMemento[resource.toString()];
const fileViewState = editorViewStateMemento[resource.toString()];
if (fileViewState) {
return fileViewState[this.position];
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#262626"><circle cx="3.5" cy="7.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="13.5" cy="7.5" r="2.5"/></g><g fill="#C5C5C5"><circle cx="3.5" cy="7.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="13.5" cy="7.5" r="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#F6F6F6"><circle cx="3.5" cy="7.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="13.5" cy="7.5" r="2.5"/></g><g fill="#424242"><circle cx="3.5" cy="7.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="13.5" cy="7.5" r="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 16 16"
version="1.1"
id="svg3828"
sodipodi:docname="left-inverse.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<style
id="style3821">.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}</style>
<path
class="icon-canvas-transparent"
d="M16 16H0V0h16v16z"
id="canvas" />
<path
class="icon-vs-out"
d="M.379 5.652l2.828-2.828 4.76 4.761 4.826-4.826 2.828 2.828-7.654 7.654L.379 5.652z"
id="outline"
style="display: none;" />
<g
id="iconBg"
transform="rotate(90,7.8976546,8.1705758)">
<path
class="icon-vs-bg"
d="M 7.967,11.827 1.793,5.652 3.207,4.238 l 4.76,4.761 4.826,-4.826 1.414,1.414 z"
id="path3825"
inkscape:connector-curvature="0"
style="fill:#c5c5c5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 16 16"
version="1.1"
id="svg3828"
sodipodi:docname="left.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<style
id="style3821">.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}</style>
<path
class="icon-canvas-transparent"
d="M16 16H0V0h16v16z"
id="canvas" />
<path
class="icon-vs-out"
d="M.379 5.652l2.828-2.828 4.76 4.761 4.826-4.826 2.828 2.828-7.654 7.654L.379 5.652z"
id="outline"
style="display: none;" />
<g
id="iconBg"
transform="rotate(90,7.8976546,8.1705758)">
<path
class="icon-vs-bg"
d="M 7.967,11.827 1.793,5.652 3.207,4.238 l 4.76,4.761 4.826,-4.826 1.414,1.414 z"
id="path3825"
inkscape:connector-curvature="0"
style="fill:#424242" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#2b282e;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>BottomRowOfTwoRows_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,0V15H1V0Z" style="display: none;"/><path class="icon-vs-fg" d="M14,2V8H3V2Z" style="display: none;"/><path class="icon-vs-bg" d="M2,1V14H15V1ZM14,8H3V2H14Z"/></svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-bg{fill:#424242;}</style></defs><title>BottomRowOfTwoRows_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,0V15H1V0Z" style="display: none;"/><path class="icon-vs-fg" d="M14,2V8H3V2Z" style="display: none;"/><path class="icon-vs-bg" d="M2,1V14H15V1ZM14,8H3V2H14Z"/></svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#2b282e;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>RightColumnOfTwoColumns_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,0V15H1V0Z" style="display: none;"/><path class="icon-vs-fg" d="M9,2V13H3V2Z" style="display: none;"/><path class="icon-vs-bg" d="M2,1V14H15V1ZM9,13H3V2H9Z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-bg{fill:#424242;}</style></defs><title>RightColumnOfTwoColumns_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M16,0V15H1V0Z" style="display: none;"/><path class="icon-vs-fg" d="M9,2V13H3V2Z" style="display: none;"/><path class="icon-vs-bg" d="M2,1V14H15V1ZM9,13H3V2H9Z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -13,10 +13,25 @@
}
.monaco-workbench > .part.panel .title {
border-top-width: 1px;
border-top-style: solid;
padding-right: 0px;
height: 35px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.monaco-workbench > .part.panel.bottom .title {
border-top-width: 1px;
border-top-style: solid;
}
.monaco-workbench > .part.panel.right {
border-left-width: 1px;
border-left-style: solid;
}
.monaco-workbench > .part.panel > .composite.title > .title-actions {
flex: 0;
}
.monaco-workbench > .part.panel > .title > .title-actions .monaco-action-bar .action-item .action-label {
@@ -25,8 +40,17 @@
/** Panel Switcher */
.monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more {
background: url('ellipsis.svg') center center no-repeat;
display: block;
height: 31px;
min-width: 28px;
margin-left: 0px;
margin-right: 0px;
}
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar {
line-height: 35px;
line-height: 32px;
}
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:first-child .action-label {
@@ -41,10 +65,22 @@
padding-bottom: 4px; /* puts the bottom border down */
}
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked {
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label {
border-bottom: 1px solid;
}
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge .badge-content {
top: 8px;
right: 0px;
position: absolute;
font-size: 11px;
min-width: 6px;
line-height: 18px;
padding: 0 5px;
border-radius: 20px;
text-align: center;
}
/** Actions */
.monaco-workbench .panel .monaco-action-bar .action-item.select-container {
@@ -53,6 +89,7 @@
.monaco-workbench .panel .monaco-action-bar .action-item .select-box {
cursor: pointer;
min-width: 110px;
}
.monaco-workbench .hide-panel-action {
@@ -60,24 +97,65 @@
}
.monaco-workbench .maximize-panel-action {
background: url('up.svg') center center no-repeat;
background-image: url('up.svg');
}
.monaco-workbench .panel.right .maximize-panel-action {
background-image: url('left.svg');
}
.vs-dark .monaco-workbench .maximize-panel-action,
.hc-black .monaco-workbench .maximize-panel-action {
background: url('up-inverse.svg') center center no-repeat;
background-image: url('up-inverse.svg');
}
.vs-dark .monaco-workbench .panel.right .maximize-panel-action,
.hc-black .monaco-workbench .panel.right .maximize-panel-action {
background-image: url('left-inverse.svg');
}
.monaco-workbench .minimize-panel-action {
background: url('down.svg') center center no-repeat;
background-image: url('down.svg');
}
.monaco-workbench .panel.right .minimize-panel-action {
background-image: url('right.svg');
}
.vs-dark .monaco-workbench .minimize-panel-action,
.hc-black .monaco-workbench .minimize-panel-action {
background: url('down-inverse.svg') center center no-repeat;
background-image: url('down-inverse.svg');
}
.vs-dark .monaco-workbench .panel.right .minimize-panel-action,
.hc-black .monaco-workbench .panel.right .minimize-panel-action {
background-image: url('right-inverse.svg');
}
.monaco-workbench .move-panel-to-right {
background: url('panel-right.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .move-panel-to-right,
.hc-black .monaco-workbench .move-panel-to-right {
background: url('panel-right-inverse.svg') center center no-repeat;
}
.monaco-workbench .move-panel-to-bottom {
background: url('panel-bottom.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .move-panel-to-bottom,
.hc-black .monaco-workbench .move-panel-to-bottom {
background: url('panel-bottom-inverse.svg') center center no-repeat;
}
.vs-dark .monaco-workbench .hide-panel-action,
.hc-black .monaco-workbench .hide-panel-action {
background: url('close-inverse.svg') center center no-repeat;
}
}
.vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more,
.hc-black .monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more {
background: url('ellipsis-inverse.svg') center center no-repeat;
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 16 16"
version="1.1"
id="svg3828"
sodipodi:docname="right-inverse.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<style
id="style3821">.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}</style>
<path
class="icon-canvas-transparent"
d="M16 16H0V0h16v16z"
id="canvas" />
<path
class="icon-vs-out"
d="M.379 5.652l2.828-2.828 4.76 4.761 4.826-4.826 2.828 2.828-7.654 7.654L.379 5.652z"
id="outline"
style="display: none;" />
<g
id="iconBg"
transform="rotate(-90,8.1705757,8.1023454)">
<path
class="icon-vs-bg"
d="M 7.967,11.827 1.793,5.652 3.207,4.238 l 4.76,4.761 4.826,-4.826 1.414,1.414 z"
id="path3825"
inkscape:connector-curvature="0"
style="fill:#c5c5c5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 16 16"
version="1.1"
id="svg3828"
sodipodi:docname="right.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<style
id="style3821">.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}</style>
<path
class="icon-canvas-transparent"
d="M16 16H0V0h16v16z"
id="canvas" />
<path
class="icon-vs-out"
d="M.379 5.652l2.828-2.828 4.76 4.761 4.826-4.826 2.828 2.828-7.654 7.654L.379 5.652z"
id="outline"
style="display: none;" />
<g
id="iconBg"
transform="rotate(-90,8.1705757,8.1023454)">
<path
class="icon-vs-bg"
d="M 7.967,11.827 1.793,5.652 3.207,4.238 l 4.76,4.761 4.826,-4.826 1.414,1.414 z"
id="path3825"
inkscape:connector-curvature="0"
style="fill:#424242" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -11,45 +11,12 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { Action } from 'vs/base/common/actions';
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/actionRegistry';
import { IPanelService, IPanelIdentifier } from 'vs/workbench/services/panel/common/panelService';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
export class PanelAction extends Action {
constructor(
private panel: IPanelIdentifier,
@IKeybindingService private keybindingService: IKeybindingService,
@IPanelService private panelService: IPanelService
) {
super(panel.id, panel.name);
this.tooltip = nls.localize('panelActionTooltip', "{0} ({1})", panel.name, this.getKeybindingLabel(panel.commandId));
}
public run(event): TPromise<any> {
return this.panelService.openPanel(this.panel.id, true).then(() => this.activate());
}
public activate(): void {
if (!this.checked) {
this._setChecked(true);
}
}
public deactivate(): void {
if (this.checked) {
this._setChecked(false);
}
}
private getKeybindingLabel(id: string): string {
const keys = this.keybindingService.lookupKeybinding(id);
return keys ? keys.getLabel() : '';
}
}
import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/actions';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService';
import { ActivityAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { IActivity } from 'vs/workbench/common/activity';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
export class ClosePanelAction extends Action {
static ID = 'workbench.action.closePanel';
@@ -115,6 +82,46 @@ class FocusPanelAction extends Action {
}
}
export class TogglePanelPositionAction extends Action {
public static ID = 'workbench.action.togglePanelPosition';
public static LABEL = nls.localize('toggledPanelPosition', "Toggle Panel Position");
private static MOVE_TO_RIGHT_LABEL = nls.localize('moveToRight', "Move to Right");
private static MOVE_TO_BOTTOM_LABEL = nls.localize('moveToBottom', "Move to Bottom");
private static panelPositionConfigurationKey = 'workbench.panel.location';
private toDispose: IDisposable[];
constructor(
id: string,
label: string,
@IPartService private partService: IPartService,
@IConfigurationService private configurationService: IConfigurationService
) {
super(id, label, partService.getPanelPosition() === Position.RIGHT ? 'move-panel-to-bottom' : 'move-panel-to-right');
this.toDispose = [];
const setClassAndLabel = () => {
const positionRight = this.partService.getPanelPosition() === Position.RIGHT;
this.class = positionRight ? 'move-panel-to-bottom' : 'move-panel-to-right';
this.label = positionRight ? TogglePanelPositionAction.MOVE_TO_BOTTOM_LABEL : TogglePanelPositionAction.MOVE_TO_RIGHT_LABEL;
};
this.toDispose.push(partService.onEditorLayout(() => setClassAndLabel()));
setClassAndLabel();
}
public run(): TPromise<any> {
const position = this.partService.getPanelPosition();
const newPositionValue = (position === Position.BOTTOM) ? 'right' : 'bottom';
return this.configurationService.updateValue(TogglePanelPositionAction.panelPositionConfigurationKey, newPositionValue, ConfigurationTarget.USER);
}
public dispose(): void {
super.dispose();
this.toDispose = dispose(this.toDispose);
}
}
export class ToggleMaximizedPanelAction extends Action {
public static ID = 'workbench.action.toggleMaximizedPanel';
@@ -139,7 +146,7 @@ export class ToggleMaximizedPanelAction extends Action {
public run(): TPromise<any> {
// Show panel
return this.partService.setPanelHidden(false)
return (!this.partService.isVisible(Parts.PANEL_PART) ? this.partService.setPanelHidden(false) : TPromise.as(null))
.then(() => this.partService.toggleMaximizedPanel());
}
@@ -149,8 +156,24 @@ export class ToggleMaximizedPanelAction extends Action {
}
}
export class PanelActivityAction extends ActivityAction {
constructor(
activity: IActivity,
@IPanelService private panelService: IPanelService
) {
super(activity);
}
public run(event: any): TPromise<any> {
return this.panelService.openPanel(this.activity.id, true).then(() => this.activate());
}
}
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TogglePanelAction, TogglePanelAction.ID, TogglePanelAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_J }), 'View: Toggle Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPanelAction, FocusPanelAction.ID, FocusPanelAction.LABEL), 'View: Focus into Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL), 'View: Toggle Maximized Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL), 'View: Close Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL), 'View: Close Panel', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL), 'View: Toggle Panel Position', nls.localize('view', "View"));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, undefined), 'View: Toggle Panel Position', nls.localize('view', "View"));

View File

@@ -4,40 +4,45 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/panelpart';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction } from 'vs/base/common/actions';
import { IAction, Action } from 'vs/base/common/actions';
import Event from 'vs/base/common/event';
import { Builder, $ } from 'vs/base/browser/builder';
import { Builder, Dimension } from 'vs/base/browser/builder';
import { Registry } from 'vs/platform/registry/common/platform';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Scope } from 'vs/workbench/browser/actions';
import { IPanel } from 'vs/workbench/common/panel';
import { CompositePart, ICompositeTitleLabel } from 'vs/workbench/browser/parts/compositePart';
import { Panel, PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel';
import { IPanelService, IPanelIdentifier } from 'vs/workbench/services/panel/common/panelService';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMessageService } from 'vs/platform/message/common/message';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { ClosePanelAction, PanelAction, ToggleMaximizedPanelAction } from 'vs/workbench/browser/parts/panel/panelActions';
import { ClosePanelAction, TogglePanelPositionAction, PanelActivityAction, ToggleMaximizedPanelAction } from 'vs/workbench/browser/parts/panel/panelActions';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER } from 'vs/workbench/common/theme';
import { activeContrastBorder, focusBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry';
import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar';
import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IBadge } from 'vs/workbench/services/activity/common/activity';
export class PanelPart extends CompositePart<Panel> implements IPanelService {
public static activePanelSettingsKey = 'workbench.panelpart.activepanelid';
private static readonly PINNED_PANELS = 'workbench.panel.pinnedPanels';
private static readonly MIN_COMPOSITE_BAR_WIDTH = 50;
public _serviceBrand: any;
private blockOpeningPanel: boolean;
private panelSwitcherBar: ActionBar;
private panelIdToActions: { [panelId: string]: PanelAction; };
private compositeBar: CompositeBar;
private dimension: Dimension;
constructor(
id: string,
@@ -48,7 +53,7 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
@IPartService partService: IPartService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
) {
super(
messageService,
@@ -70,7 +75,26 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
{ hasTitle: true }
);
this.panelIdToActions = Object.create(null);
this.compositeBar = this.instantiationService.createInstance(CompositeBar, {
icon: false,
storageId: PanelPart.PINNED_PANELS,
orientation: ActionsOrientation.HORIZONTAL,
composites: this.getPanels(),
openComposite: (compositeId: string) => this.openPanel(compositeId, true),
getActivityAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, this.getPanel(compositeId)),
getCompositePinnedAction: (compositeId: string) => new ToggleCompositePinnedAction(this.getPanel(compositeId), this.compositeBar),
getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, this.getPanel(compositeId)),
getDefaultCompositeId: () => Registry.as<PanelRegistry>(PanelExtensions.Panels).getDefaultPanelId(),
hidePart: () => this.partService.setPanelHidden(true),
overflowActionSize: 28,
colors: {
backgroundColor: PANEL_BACKGROUND,
badgeBackground,
badgeForeground,
dragAndDropBackground: PANEL_DRAG_AND_DROP_BACKGROUND
}
});
this.toUnbind.push(this.compositeBar);
this.registerListeners();
}
@@ -78,16 +102,15 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
private registerListeners(): void {
// Activate panel action on opening of a panel
this.toUnbind.push(this.onDidPanelOpen(panel => this.updatePanelActions(panel.getId(), true)));
this.toUnbind.push(this.onDidPanelOpen(panel => {
this.compositeBar.activateComposite(panel.getId());
// Need to relayout composite bar since different panels have different action bar width
this.layoutCompositeBar();
}));
// Deactivate panel action on close
this.toUnbind.push(this.onDidPanelClose(panel => this.updatePanelActions(panel.getId(), false)));
}
private updatePanelActions(id: string, didOpen: boolean): void {
if (this.panelIdToActions[id]) {
didOpen ? this.panelIdToActions[id].activate() : this.panelIdToActions[id].deactivate();
}
this.toUnbind.push(this.onDidPanelClose(panel => this.compositeBar.deactivateComposite(panel.getId())));
this.toUnbind.push(this.compositeBar.onDidContextMenu(e => this.showContextMenu(e)));
}
public get onDidPanelOpen(): Event<IPanel> {
@@ -98,11 +121,12 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
return this._onDidCompositeClose.event;
}
protected updateStyles(): void {
public updateStyles(): void {
super.updateStyles();
const container = this.getContainer();
container.style('background-color', this.getColor(PANEL_BACKGROUND));
container.style('border-left-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder));
const title = this.getTitleArea();
title.style('border-top-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder));
@@ -127,6 +151,25 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
return promise.then(() => this.openComposite(id, focus));
}
public showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable {
return this.compositeBar.showActivity(panelId, badge, clazz);
}
private getPanel(panelId: string): IPanelIdentifier {
return Registry.as<PanelRegistry>(PanelExtensions.Panels).getPanels().filter(p => p.id === panelId).pop();
}
private showContextMenu(e: MouseEvent): void {
const event = new StandardMouseEvent(e);
const actions: Action[] = this.getPanels().map(panel => this.instantiationService.createInstance(ToggleCompositePinnedAction, panel, this.compositeBar));
this.contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => TPromise.as(actions),
onHide: () => dispose(actions)
});
}
public getPanels(): IPanelIdentifier[] {
return Registry.as<PanelRegistry>(PanelExtensions.Panels).getPanels()
.sort((v1, v2) => v1.order - v2.order);
@@ -135,6 +178,7 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
protected getActions(): IAction[] {
return [
this.instantiationService.createInstance(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL),
this.instantiationService.createInstance(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL),
this.instantiationService.createInstance(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL)
];
}
@@ -152,23 +196,12 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
}
protected createTitleLabel(parent: Builder): ICompositeTitleLabel {
let titleArea = $(parent).div({
'class': ['panel-switcher-container']
});
// Show a panel switcher
this.panelSwitcherBar = new ActionBar(titleArea, {
orientation: ActionsOrientation.HORIZONTAL,
ariaLabel: nls.localize('panelSwitcherBarAriaLabel', "Active Panel Switcher"),
animated: false
});
this.toUnbind.push(this.panelSwitcherBar);
this.fillPanelSwitcher();
const titleArea = this.compositeBar.create(parent.getHTMLElement());
titleArea.classList.add('panel-switcher-container');
return {
updateTitle: (id, title, keybinding) => {
const action = this.panelIdToActions[id];
const action = this.compositeBar.getAction(id);
if (action) {
action.label = title;
}
@@ -179,17 +212,37 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
};
}
private fillPanelSwitcher(): void {
const panels = this.getPanels();
public layout(dimension: Dimension): Dimension[] {
this.panelSwitcherBar.push(panels.map(panel => {
const action = this.instantiationService.createInstance(PanelAction, panel);
if (this.partService.getPanelPosition() === Position.RIGHT) {
// Take into account the 1px border when layouting
this.dimension = new Dimension(dimension.width - 1, dimension.height);
} else {
this.dimension = dimension;
}
const sizes = super.layout(this.dimension);
this.layoutCompositeBar();
this.panelIdToActions[panel.id] = action;
this.toUnbind.push(action);
return sizes;
}
return action;
}));
private layoutCompositeBar(): void {
if (this.dimension) {
let availableWidth = this.dimension.width - 8; // take padding into account
if (this.toolBar) {
// adjust height for global actions showing
availableWidth = Math.max(PanelPart.MIN_COMPOSITE_BAR_WIDTH, availableWidth - this.toolBar.getContainer().getHTMLElement().offsetWidth);
}
this.compositeBar.layout(new Dimension(availableWidth, this.dimension.height));
}
}
public shutdown(): void {
// Persist Hidden State
this.compositeBar.store();
// Pass to super
super.shutdown();
}
}
@@ -215,7 +268,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
if (titleActive || titleActiveBorder) {
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label,
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked {
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label {
color: ${titleActive};
border-bottom-color: ${titleActiveBorder};
}
@@ -236,13 +289,17 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const focusBorderColor = theme.getColor(focusBorder);
if (focusBorderColor) {
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:focus {
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus .action-label {
color: ${titleActive};
border-bottom-color: ${focusBorderColor} !important;
border-bottom: 1px solid;
}
`);
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus {
outline: none;
}
`);
`);
}
// Styling with Outline color (e.g. high contrast theme)
@@ -251,7 +308,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const outline = theme.getColor(activeContrastBorder);
collector.addRule(`
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label.checked,
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label,
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:hover {
outline-color: ${outline};
outline-width: 1px;
@@ -261,7 +318,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
outline-offset: 3px;
}
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:hover:not(.checked) {
.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:not(.checked) .action-label:hover {
outline-style: dashed;
}
`);

View File

@@ -14,13 +14,14 @@ import strings = require('vs/base/common/strings');
import filters = require('vs/base/common/filters');
import DOM = require('vs/base/browser/dom');
import URI from 'vs/base/common/uri';
import * as resources from 'vs/base/common/resources';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import types = require('vs/base/common/types');
import { Action, IAction } from 'vs/base/common/actions';
import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, compareEntries, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenWidget, HideReason } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import labels = require('vs/base/common/labels');
@@ -31,12 +32,12 @@ import { IResourceInput, IEditorInput } from 'vs/platform/editor/common/editor';
import { IModeService } from 'vs/editor/common/services/modeService';
import { getIconClasses } from 'vs/workbench/browser/labels';
import { IModelService } from 'vs/editor/common/services/modelService';
import { EditorInput, toResource, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { Component } from 'vs/workbench/common/component';
import Event, { Emitter } from 'vs/base/common/event';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { KeyMod } from 'vs/base/common/keyCodes';
import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen';
import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG } from 'vs/workbench/browser/quickopen';
import errors = require('vs/base/common/errors');
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPickOpenEntry, IFilePickOpenEntry, IInputOptions, IQuickOpenService, IPickOptions, IShowOptions, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen';
@@ -54,7 +55,8 @@ import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree';
import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { FileKind } from 'vs/platform/files/common/files';
import { FileKind, IFileService } from 'vs/platform/files/common/files';
import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
const HELP_PREFIX = '?';
@@ -99,6 +101,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
private previousValue = '';
private visibilityChangeTimeoutHandle: number;
private closeOnFocusLost: boolean;
private editorHistoryHandler: EditorHistoryHandler;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@@ -111,6 +114,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
@IInstantiationService private instantiationService: IInstantiationService,
@IPartService private partService: IPartService,
@IListService private listService: IListService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IThemeService themeService: IThemeService
) {
super(QuickOpenController.ID, themeService);
@@ -121,24 +125,30 @@ export class QuickOpenController extends Component implements IQuickOpenService
this.promisesToCompleteOnHide = [];
this.editorHistoryHandler = this.instantiationService.createInstance(EditorHistoryHandler);
this.inQuickOpenMode = new RawContextKey<boolean>('inQuickOpen', false).bindTo(contextKeyService);
this._onShow = new Emitter<void>();
this._onHide = new Emitter<void>();
this.updateConfiguration(<IWorkbenchQuickOpenConfiguration>this.configurationService.getConfiguration());
this.updateConfiguration();
this.registerListeners();
}
private registerListeners(): void {
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.updateConfiguration(this.configurationService.getConfiguration<IWorkbenchQuickOpenConfiguration>())));
this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.updateConfiguration()));
this.toUnbind.push(this.partService.onTitleBarVisibilityChange(() => this.positionQuickOpenWidget()));
this.toUnbind.push(browser.onDidChangeZoomLevel(() => this.positionQuickOpenWidget()));
}
private updateConfiguration(settings: IWorkbenchQuickOpenConfiguration): void {
this.closeOnFocusLost = settings.workbench && settings.workbench.quickOpen && settings.workbench.quickOpen.closeOnFocusLost;
private updateConfiguration(): void {
if (this.environmentService.args['sticky-quickopen']) {
this.closeOnFocusLost = false;
} else {
this.closeOnFocusLost = this.configurationService.getValue(CLOSE_ON_FOCUS_LOST_CONFIG);
}
}
public get onShow(): Event<void> {
@@ -169,7 +179,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
: nls.localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel");
let currentPick = defaultMessage;
let currentValidation = TPromise.as(true);
let currentValidation: TPromise<boolean>;
let currentDecoration: Severity;
let lastValue: string;
@@ -186,35 +196,49 @@ export class QuickOpenController extends Component implements IQuickOpenService
valueSelection: options.valueSelection,
inputDecoration: currentDecoration,
onDidType: (value) => {
lastValue = value;
if (lastValue !== value) {
if (options.validateInput) {
if (currentValidation) {
currentValidation.cancel();
}
lastValue = value;
currentValidation = TPromise.timeout(100).then(() => {
return options.validateInput(value).then(message => {
currentDecoration = !!message ? Severity.Error : void 0;
const newPick = message || defaultMessage;
if (newPick !== currentPick) {
options.valueSelection = [lastValue.length, lastValue.length];
currentPick = newPick;
resolve(new TPromise<any>(init));
}
if (options.validateInput) {
if (currentValidation) {
currentValidation.cancel();
}
return !message;
currentValidation = TPromise.timeout(100).then(() => {
return options.validateInput(value).then(message => {
currentDecoration = !!message ? Severity.Error : void 0;
const newPick = message || defaultMessage;
if (newPick !== currentPick) {
options.valueSelection = [lastValue.length, lastValue.length];
currentPick = newPick;
resolve(new TPromise<any>(init));
}
return !message;
});
}, err => {
// ignore
return null;
});
}, err => {
// ignore
return null;
});
}
}
}
}, token).then(resolve, reject);
};
return new TPromise(init).then(item => {
if (!currentValidation) {
if (options.validateInput) {
currentValidation = options
.validateInput(lastValue === void 0 ? options.value : lastValue)
.then(message => !message);
} else {
currentValidation = TPromise.as(true);
}
}
return currentValidation.then(valid => {
if (valid && item) {
return lastValue === void 0 ? (options.value || '') : lastValue;
@@ -439,7 +463,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
return pickA.index - pickB.index; // restore natural order
}
return QuickOpenEntry.compare(pickA, pickB, normalizedSearchValue);
return compareEntries(pickA, pickB, normalizedSearchValue);
});
this.pickOpenWidget.refresh(model, value ? { autoFocusFirstEntry: true } : autoFocus);
@@ -525,6 +549,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
public show(prefix?: string, options?: IShowOptions): TPromise<void> {
let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0;
let inputSelection = options ? options.inputSelection : void 0;
let autoFocus = options ? options.autoFocus : void 0;
this.previousValue = prefix;
@@ -536,11 +561,16 @@ export class QuickOpenController extends Component implements IQuickOpenService
const registry = Registry.as<IQuickOpenRegistry>(Extensions.Quickopen);
const handlerDescriptor = registry.getQuickOpenHandler(prefix) || registry.getDefaultQuickOpenHandler();
/* __GDPR__
"quickOpenWidgetShown" : {
"mode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"quickNavigate": { "${inline}": [ "${IQuickNavigateConfiguration}" ] }
}
*/
this.telemetryService.publicLog('quickOpenWidgetShown', { mode: handlerDescriptor.getId(), quickNavigate: quickNavigateConfiguration });
// Trigger onOpen
this.resolveHandler(handlerDescriptor)
.done(null, errors.onUnexpectedError);
this.resolveHandler(handlerDescriptor).done(null, errors.onUnexpectedError);
// Create upon first open
if (!this.quickOpenWidget) {
@@ -575,19 +605,21 @@ export class QuickOpenController extends Component implements IQuickOpenService
// Show quick open with prefix or editor history
if (!this.quickOpenWidget.isVisible() || quickNavigateConfiguration) {
if (prefix) {
this.quickOpenWidget.show(prefix, { quickNavigateConfiguration, inputSelection });
this.quickOpenWidget.show(prefix, { quickNavigateConfiguration, inputSelection, autoFocus });
} else {
const editorHistory = this.getEditorHistoryWithGroupLabel();
if (editorHistory.getEntries().length < 2) {
quickNavigateConfiguration = null; // If no entries can be shown, default to normal quick open mode
}
let autoFocus: IAutoFocus;
if (!quickNavigateConfiguration) {
autoFocus = { autoFocusFirstEntry: true };
} else {
const visibleEditorCount = this.editorService.getVisibleEditors().length;
autoFocus = { autoFocusFirstEntry: visibleEditorCount === 0, autoFocusSecondEntry: visibleEditorCount !== 0 };
// Compute auto focus
if (!autoFocus) {
if (!quickNavigateConfiguration) {
autoFocus = { autoFocusFirstEntry: true };
} else {
const visibleEditorCount = this.editorService.getVisibleEditors().length;
autoFocus = { autoFocusFirstEntry: visibleEditorCount === 0, autoFocusSecondEntry: visibleEditorCount !== 0 };
}
}
// Update context
@@ -695,7 +727,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
}
private getEditorHistoryWithGroupLabel(): QuickOpenModel {
const entries: QuickOpenEntry[] = this.getEditorHistoryEntries();
const entries: QuickOpenEntry[] = this.editorHistoryHandler.getResults();
// Apply label to first entry
if (entries.length > 0) {
@@ -793,7 +825,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
private handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, currentResultToken: string): TPromise<void> {
// Fill in history results if matching
const matchingHistoryEntries = this.getEditorHistoryEntries(value);
const matchingHistoryEntries = this.editorHistoryHandler.getResults(value);
if (matchingHistoryEntries.length > 0) {
matchingHistoryEntries[0] = new EditorHistoryEntryGroup(matchingHistoryEntries[0], nls.localize('historyMatches', "recently opened"), false);
}
@@ -835,59 +867,6 @@ export class QuickOpenController extends Component implements IQuickOpenService
});
}
private getEditorHistoryEntries(searchValue?: string): QuickOpenEntry[] {
if (searchValue) {
searchValue = searchValue.replace(/ /g, ''); // get rid of all whitespace
}
// Just return all if we are not searching
const history = this.historyService.getHistory();
if (!searchValue) {
return history.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input));
}
const searchInPath = searchValue.indexOf(paths.nativeSep) >= 0;
const results: QuickOpenEntry[] = [];
history.forEach(input => {
let resource: URI;
if (input instanceof EditorInput) {
resource = toResource(input, { filter: ['file', 'untitled'] });
} else {
resource = (input as IResourceInput).resource;
}
if (!resource) {
return; //For now, only support to match on inputs that provide resource information
}
let searchTargetToMatch: string;
if (searchInPath) {
searchTargetToMatch = labels.getPathLabel(resource, this.contextService);
} else if (input instanceof EditorInput) {
searchTargetToMatch = input.getName();
} else {
searchTargetToMatch = paths.basename((input as IResourceInput).resource.fsPath);
}
// Check if this entry is a match for the search value
if (!filters.matchesFuzzy(searchValue, searchTargetToMatch)) {
return;
}
const entry = this.instantiationService.createInstance(EditorHistoryEntry, input);
const { labelHighlights, descriptionHighlights } = QuickOpenEntry.highlight(entry, searchValue);
entry.setHighlights(labelHighlights, descriptionHighlights);
results.push(entry);
});
// Sort
const normalizedSearchValue = strings.stripWildcards(searchValue.toLowerCase());
return results.sort((elementA: EditorHistoryEntry, elementB: EditorHistoryEntry) => QuickOpenEntry.compare(elementA, elementB, normalizedSearchValue));
}
private mergeResults(quickOpenModel: QuickOpenModel, handlerResults: QuickOpenEntry[], groupLabel: string): void {
// Remove results already showing by checking for a "resource" property
@@ -1007,7 +986,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
return result.then<QuickOpenHandler>(null, (error) => {
delete this.mapResolvedHandlersToPrefix[id];
return TPromise.wrapError(new Error('Unable to instantiate quick open handler ' + handler.moduleName + ' - ' + handler.ctorName + ': ' + JSON.stringify(error)));
return TPromise.wrapError(new Error(`Unable to instantiate quick open handler ${handler.getId()}: ${JSON.stringify(error)}`));
});
}
@@ -1020,7 +999,7 @@ export class QuickOpenController extends Component implements IQuickOpenService
}
// Otherwise load and create
return this.mapResolvedHandlersToPrefix[id] = this.instantiationService.createInstance<QuickOpenHandler>(handler);
return this.mapResolvedHandlersToPrefix[id] = TPromise.as(handler.instantiate(this.instantiationService));
}
public layout(dimension: Dimension): void {
@@ -1191,6 +1170,81 @@ class PickOpenActionProvider implements IActionProvider {
}
}
class EditorHistoryHandler {
private scorerCache: ScorerCache;
constructor(
@IHistoryService private historyService: IHistoryService,
@IInstantiationService private instantiationService: IInstantiationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IFileService private fileService: IFileService
) {
this.scorerCache = Object.create(null);
}
public getResults(searchValue?: string): QuickOpenEntry[] {
// Massage search for scoring
const query = prepareQuery(searchValue);
// Just return all if we are not searching
const history = this.historyService.getHistory();
if (!query.value) {
return history.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input));
}
// Otherwise filter by search value and sort by score. Include matches on description
// in case the user is explicitly including path separators.
const accessor = query.containsPathSeparator ? MatchOnDescription : DoNotMatchOnDescription;
return history
// For now, only support to match on inputs that provide resource information
.filter(input => {
let resource: URI;
if (input instanceof EditorInput) {
resource = resourceForEditorHistory(input, this.fileService);
} else {
resource = (input as IResourceInput).resource;
}
return !!resource;
})
// Conver to quick open entries
.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input))
// Make sure the search value is matching
.filter(e => {
const itemScore = scoreItem(e, query, false, accessor, this.scorerCache);
if (!itemScore.score) {
return false;
}
e.setHighlights(itemScore.labelMatch, itemScore.descriptionMatch);
return true;
})
// Sort by score and provide a fallback sorter that keeps the
// recency of items in case the score for items is the same
.sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache, (e1, e2, query, accessor) => -1));
}
}
class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass {
constructor(private allowMatchOnDescription: boolean) {
super();
}
public getItemDescription(entry: QuickOpenEntry): string {
return this.allowMatchOnDescription ? entry.getDescription() : void 0;
}
}
const MatchOnDescription = new EditorHistoryItemAccessorClass(true);
const DoNotMatchOnDescription = new EditorHistoryItemAccessorClass(false);
export class EditorHistoryEntryGroup extends QuickOpenEntryGroup {
// Marker class
}
@@ -1210,14 +1264,15 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry {
@ITextFileService private textFileService: ITextFileService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IConfigurationService private configurationService: IConfigurationService,
@IEnvironmentService environmentService: IEnvironmentService
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService private fileService: IFileService
) {
super(editorService);
this.input = input;
if (input instanceof EditorInput) {
this.resource = toResource(input, { filter: ['file', 'untitled'] });
this.resource = resourceForEditorHistory(input, fileService);
this.label = input.getName();
this.description = input.getDescription();
this.dirty = input.isDirty();
@@ -1225,7 +1280,7 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry {
const resourceInput = input as IResourceInput;
this.resource = resourceInput.resource;
this.label = paths.basename(resourceInput.resource.fsPath);
this.description = labels.getPathLabel(paths.dirname(this.resource.fsPath), contextService, environmentService);
this.description = labels.getPathLabel(resources.dirname(this.resource), contextService, environmentService);
this.dirty = this.resource && this.textFileService.isDirty(this.resource);
if (this.dirty && this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
@@ -1282,6 +1337,18 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry {
}
}
function resourceForEditorHistory(input: EditorInput, fileService: IFileService): URI {
const resource = input ? input.getResource() : void 0;
// For the editor history we only prefer resources that are either untitled or
// can be handled by the file service which indicates they are editable resources.
if (resource && (fileService.canHandleResource(resource) || resource.scheme === 'untitled')) {
return resource;
}
return void 0;
}
export class RemoveFromEditorHistoryAction extends Action {
public static ID = 'workbench.action.removeFromEditorHistory';

View File

@@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { RemoveFromEditorHistoryAction } from 'vs/workbench/browser/parts/quickopen/quickOpenController';
import { QuickOpenSelectNextAction, QuickOpenSelectPreviousAction, inQuickOpenContext, getQuickNavigateHandler, QuickOpenNavigateNextAction, QuickOpenNavigatePreviousAction, defaultQuickOpenContext, QUICKOPEN_ACTION_ID, QUICKOPEN_ACION_LABEL } from 'vs/workbench/browser/parts/quickopen/quickopen';

View File

@@ -29,6 +29,15 @@ CommandsRegistry.registerCommand(QUICKOPEN_ACTION_ID, function (accessor: Servic
});
});
export const QUICKOPEN_FOCUS_SECONDARY_ACTION_ID = 'workbench.action.quickOpenPreviousEditor';
CommandsRegistry.registerCommand(QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, function (accessor: ServicesAccessor, prefix: string = null) {
const quickOpenService = accessor.get(IQuickOpenService);
return quickOpenService.show(null, { autoFocus: { autoFocusSecondEntry: true } }).then(() => {
return void 0;
});
});
export class BaseQuickOpenNavigateAction extends Action {
constructor(

View File

@@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Action, IAction } from 'vs/base/common/actions';
import { CompositePart } from 'vs/workbench/browser/parts/compositePart';
import { Viewlet, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';

View File

@@ -17,7 +17,6 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { Part } from 'vs/workbench/browser/part';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { StatusbarAlignment, IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -28,7 +27,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { Action } from 'vs/base/common/actions';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { isThemeColor } from 'vs/editor/common/editorCommon';
import { Color } from 'vs/base/common/color';
@@ -55,7 +54,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
}
private registerListeners(): void {
this.toUnbind.push(this.contextService.onDidChangeWorkspaceRoots(() => this.updateStyles()));
this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));
}
public addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IDisposable {
@@ -141,10 +140,10 @@ export class StatusbarPart extends Part implements IStatusbarService {
const container = this.getContainer();
container.style('color', this.getColor(this.contextService.hasWorkspace() ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND));
container.style('background-color', this.getColor(this.contextService.hasWorkspace() ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND));
container.style('color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND));
container.style('background-color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND));
const borderColor = this.getColor(this.contextService.hasWorkspace() ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder);
const borderColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder);
container.style('border-top-width', borderColor ? '1px' : null);
container.style('border-top-style', borderColor ? 'solid' : null);
container.style('border-top-color', borderColor);
@@ -287,23 +286,6 @@ class StatusBarEntryItem implements IStatusbarItem {
private executeCommand(id: string, args?: any[]) {
args = args || [];
// Lookup built in commands
const builtInActionDescriptor = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).getWorkbenchAction(id);
if (builtInActionDescriptor) {
const action = this.instantiationService.createInstance(builtInActionDescriptor.syncDescriptor);
if (action.enabled) {
this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'status bar' });
(action.run() || TPromise.as(null)).done(() => {
action.dispose();
}, (err) => this.messageService.show(Severity.Error, toErrorMessage(err)));
} else {
this.messageService.show(Severity.Warning, nls.localize('canNotRun', "Command '{0}' is currently not enabled and can not be run.", action.label || id));
}
return;
}
// Maintain old behaviour of always focusing the editor here
const activeEditor = this.editorService.getActiveEditor();
const codeEditor = getCodeEditor(activeEditor);
@@ -311,7 +293,13 @@ class StatusBarEntryItem implements IStatusbarItem {
codeEditor.focus();
}
// Fallback to the command service for any other case
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' });
this.commandService.executeCommand(id, ...args).done(undefined, err => this.messageService.show(Severity.Error, toErrorMessage(err)));
}
}
@@ -349,4 +337,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
if (statusBarProminentItemHoverBackground) {
collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item a.status-bar-info:hover:not([disabled]):not(.disabled) { background-color: ${statusBarProminentItemHoverBackground}; }`);
}
});
});

View File

@@ -18,7 +18,7 @@ import * as errors from 'vs/base/common/errors';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IAction, Action } from 'vs/base/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IIntegrityService } from 'vs/platform/integrity/common/integrity';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
@@ -27,12 +27,13 @@ import nls = require('vs/nls');
import * as labels from 'vs/base/common/labels';
import { EditorInput, toResource } from 'vs/workbench/common/editor';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Verbosity } from 'vs/platform/editor/common/editor';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme';
import URI from 'vs/base/common/uri';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { isMacintosh } from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
export class TitlebarPart extends Part implements ITitleService {
@@ -41,7 +42,7 @@ export class TitlebarPart extends Part implements ITitleService {
private static NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]");
private static NLS_EXTENSION_HOST = nls.localize('devExtensionWindowTitlePrefix', "[Extension Development Host]");
private static TITLE_DIRTY = '\u25cf ';
private static TITLE_SEPARATOR = ' — ';
private static TITLE_SEPARATOR = isMacintosh ? ' — ' : ' - '; // macOS uses special - separator
private titleContainer: Builder;
private title: Builder;
@@ -51,7 +52,6 @@ export class TitlebarPart extends Part implements ITitleService {
private isInactive: boolean;
private titleTemplate: string;
private isPure: boolean;
private activeEditorListeners: IDisposable[];
@@ -81,9 +81,6 @@ export class TitlebarPart extends Part implements ITitleService {
private init(): void {
// Read initial config
this.onConfigurationChanged();
// Initial window title when loading is done
this.partService.joinCreation().done(() => this.setTitle(this.getWindowTitle()));
@@ -99,10 +96,11 @@ export class TitlebarPart extends Part implements ITitleService {
private registerListeners(): void {
this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onBlur()));
this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.FOCUS, () => this.onFocus()));
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(() => this.onConfigurationChanged(true)));
this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
this.toUnbind.push(this.contextService.onDidChangeWorkspaceRoots(() => this.onDidChangeWorkspaceRoots()));
this.toUnbind.push(this.contextService.onDidChangeWorkspaceName(() => this.onDidChangeWorkspaceName()));
this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle())));
this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle())));
this.toUnbind.push(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle())));
}
private onBlur(): void {
@@ -115,19 +113,8 @@ export class TitlebarPart extends Part implements ITitleService {
this.updateStyles();
}
private onDidChangeWorkspaceRoots(): void {
this.setTitle(this.getWindowTitle());
}
private onDidChangeWorkspaceName(): void {
this.setTitle(this.getWindowTitle());
}
private onConfigurationChanged(update?: boolean): void {
const currentTitleTemplate = this.titleTemplate;
this.titleTemplate = this.configurationService.lookup<string>('window.title').value;
if (update && currentTitleTemplate !== this.titleTemplate) {
private onConfigurationChanged(event: IConfigurationChangeEvent): void {
if (event.affectsConfiguration('window.title')) {
this.setTitle(this.getWindowTitle());
}
}
@@ -192,41 +179,32 @@ export class TitlebarPart extends Part implements ITitleService {
const input = this.editorService.getActiveEditorInput();
const workspace = this.contextService.getWorkspace();
// Compute root resource
// Single Root Workspace: always the single root workspace in this case
// Multi Root Workspace: workspace configuration file
let root: URI;
if (this.contextService.hasMultiFolderWorkspace()) {
if (workspace.configuration) {
root = workspace.configuration;
} else if (this.contextService.hasFolderWorkspace()) {
root = workspace.roots[0];
} else if (workspace.folders.length) {
root = workspace.folders[0].uri;
}
// Compute folder resource
// Single Root Workspace: always the root single workspace in this case
// Multi Root Workspace: root folder of the currently active file if any
let folder: URI;
if (workspace) {
if (workspace.roots.length === 1) {
folder = workspace.roots[0];
} else {
folder = this.contextService.getRoot(toResource(input, { supportSideBySide: true, filter: 'file' }));
}
}
// Otherwise: root folder of the currently active file if any
let folder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? workspace.folders[0] : this.contextService.getWorkspaceFolder(toResource(input, { supportSideBySide: true }));
// Variables
const activeEditorShort = input ? input.getTitle(Verbosity.SHORT) : '';
const activeEditorMedium = input ? input.getTitle(Verbosity.MEDIUM) : activeEditorShort;
const activeEditorLong = input ? input.getTitle(Verbosity.LONG) : activeEditorMedium;
const rootName = workspace ? workspace.name : '';
const rootPath = workspace ? labels.getPathLabel(root, void 0, this.environmentService) : '';
const folderName = folder ? (paths.basename(folder.fsPath) || folder.fsPath) : '';
const folderPath = folder ? labels.getPathLabel(folder, void 0, this.environmentService) : '';
const rootName = workspace.name;
const rootPath = root ? labels.getPathLabel(root, void 0, this.environmentService) : '';
const folderName = folder ? folder.name : '';
const folderPath = folder ? labels.getPathLabel(folder.uri, void 0, this.environmentService) : '';
const dirty = input && input.isDirty() ? TitlebarPart.TITLE_DIRTY : '';
const appName = this.environmentService.appNameLong;
const separator = TitlebarPart.TITLE_SEPARATOR;
const titleTemplate = this.configurationService.getValue<string>('window.title');
return labels.template(this.titleTemplate, {
return labels.template(titleTemplate, {
activeEditorShort,
activeEditorLong,
activeEditorMedium,
@@ -265,6 +243,17 @@ export class TitlebarPart extends Part implements ITitleService {
}
});
// Since the title area is used to drag the window, we do not want to steal focus from the
// currently active element. So we restore focus after a timeout back to where it was.
this.titleContainer.on([DOM.EventType.MOUSE_DOWN], () => {
const active = document.activeElement;
setTimeout(() => {
if (active instanceof HTMLElement) {
active.focus();
}
}, 0 /* need a timeout because we are in capture phase */);
}, void 0, true /* use capture to know the currently active element properly */);
return this.titleContainer;
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-panel-view .panel > .panel-header > .title {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.custom-view-tree-node-item {
display: flex;
height: 22px;
line-height: 22px;
}
.custom-view-tree-node-item > .custom-view-tree-node-item-icon {
background-size: 16px;
background-position: left center;
background-repeat: no-repeat;
padding-right: 6px;
width: 16px;
height: 22px;
-webkit-font-smoothing: antialiased;
}
.custom-view-tree-node-item > .custom-view-tree-node-item-label {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -0,0 +1,290 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/panelviewlet';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import Event, { Emitter } from 'vs/base/common/event';
import { ColorIdentifier, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { attachStyler, IColorMapping, IThemable } from 'vs/platform/theme/common/styler';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND } from 'vs/workbench/common/theme';
import { Dimension, Builder } from 'vs/base/browser/builder';
import { append, $, trackFocus } from 'vs/base/browser/dom';
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { firstIndex } from 'vs/base/common/arrays';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Registry } from 'vs/platform/registry/common/platform';
import { prepareActions } from 'vs/workbench/browser/actions';
import { Viewlet, ViewletRegistry, Extensions } from 'vs/workbench/browser/viewlet';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { PanelView, IPanelViewOptions, IPanelOptions, Panel } from 'vs/base/browser/ui/splitview/panelview';
export interface IPanelColors extends IColorMapping {
dropBackground?: ColorIdentifier;
headerForeground?: ColorIdentifier;
headerBackground?: ColorIdentifier;
headerHighContrastBorder?: ColorIdentifier;
}
export function attachPanelStyler(widget: IThemable, themeService: IThemeService) {
return attachStyler<IPanelColors>(themeService, {
headerForeground: SIDE_BAR_SECTION_HEADER_FOREGROUND,
headerBackground: SIDE_BAR_SECTION_HEADER_BACKGROUND,
headerHighContrastBorder: contrastBorder,
dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND
}, widget);
}
export interface IViewletPanelOptions extends IPanelOptions {
actionRunner?: IActionRunner;
}
export abstract class ViewletPanel extends Panel {
private _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
protected actionRunner: IActionRunner;
protected toolbar: ToolBar;
constructor(
readonly title: string,
options: IViewletPanelOptions,
@IKeybindingService protected keybindingService: IKeybindingService,
@IContextMenuService protected contextMenuService: IContextMenuService
) {
super(options);
this.actionRunner = options.actionRunner;
}
render(container: HTMLElement): void {
super.render(container);
const focusTracker = trackFocus(container);
this.disposables.push(focusTracker);
this.disposables.push(focusTracker.addFocusListener(() => this._onDidFocus.fire()));
}
protected renderHeader(container: HTMLElement): void {
this.renderHeaderTitle(container);
const actions = append(container, $('.actions'));
this.toolbar = new ToolBar(actions, this.contextMenuService, {
orientation: ActionsOrientation.HORIZONTAL,
actionItemProvider: action => this.getActionItem(action),
ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title),
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
actionRunner: this.actionRunner
});
this.disposables.push(this.toolbar);
this.updateActions();
}
protected renderHeaderTitle(container: HTMLElement): void {
append(container, $('.title', null, this.title));
}
focus(): void {
this._onDidFocus.fire();
}
protected updateActions(): void {
this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions()))();
this.toolbar.context = this.getActionsContext();
}
getActions(): IAction[] {
return [];
}
getSecondaryActions(): IAction[] {
return [];
}
getActionItem(action: IAction): IActionItem {
return null;
}
getActionsContext(): any {
return undefined;
}
getOptimalWidth(): number {
return 0;
}
}
export interface IViewsViewletOptions extends IPanelViewOptions {
showHeaderInTitleWhenSingleView: boolean;
}
interface IViewletPanelItem {
panel: ViewletPanel;
disposable: IDisposable;
}
export class PanelViewlet extends Viewlet {
protected lastFocusedPanel: ViewletPanel | undefined;
private panelItems: IViewletPanelItem[] = [];
private panelview: PanelView;
get onDidSashChange(): Event<void> {
return this.panelview.onDidSashChange;
}
protected get length(): number {
return this.panelItems.length;
}
constructor(
id: string,
private options: IViewsViewletOptions,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService
) {
super(id, telemetryService, themeService);
}
async create(parent: Builder): TPromise<void> {
super.create(parent);
const container = parent.getHTMLElement();
this.panelview = this._register(new PanelView(container, this.options));
this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel));
}
getTitle(): string {
let title = Registry.as<ViewletRegistry>(Extensions.Viewlets).getViewlet(this.getId()).name;
if (this.isSingleView()) {
title += ': ' + this.panelItems[0].panel.title;
}
return title;
}
getActions(): IAction[] {
if (this.isSingleView()) {
return this.panelItems[0].panel.getActions();
}
return [];
}
getSecondaryActions(): IAction[] {
if (this.isSingleView()) {
return this.panelItems[0].panel.getSecondaryActions();
}
return [];
}
focus(): void {
super.focus();
if (this.lastFocusedPanel) {
this.lastFocusedPanel.focus();
} else if (this.panelItems.length > 0) {
this.panelItems[0].panel.focus();
}
}
layout(dimension: Dimension): void {
this.panelview.layout(dimension.height);
}
getOptimalWidth(): number {
const sizes = this.panelItems
.map(panelItem => panelItem.panel.getOptimalWidth() || 0);
return Math.max(...sizes);
}
addPanel(panel: ViewletPanel, size: number, index = this.panelItems.length - 1): void {
const disposables: IDisposable[] = [];
const onDidFocus = panel.onDidFocus(() => this.lastFocusedPanel = panel, null, disposables);
const styler = attachPanelStyler(panel, this.themeService);
const disposable = combinedDisposable([onDidFocus, styler]);
const panelItem: IViewletPanelItem = { panel, disposable };
this.panelItems.splice(index, 0, panelItem);
this.panelview.addPanel(panel, size, index);
this.updateViewHeaders();
this.updateTitleArea();
}
removePanel(panel: ViewletPanel): void {
const index = firstIndex(this.panelItems, i => i.panel === panel);
if (index === -1) {
return;
}
if (this.lastFocusedPanel === panel) {
this.lastFocusedPanel = undefined;
}
this.panelview.removePanel(panel);
const [panelItem] = this.panelItems.splice(index, 1);
panelItem.disposable.dispose();
this.updateViewHeaders();
this.updateTitleArea();
}
movePanel(from: ViewletPanel, to: ViewletPanel): void {
const fromIndex = firstIndex(this.panelItems, item => item.panel === from);
const toIndex = firstIndex(this.panelItems, item => item.panel === to);
if (fromIndex < 0 || fromIndex >= this.panelItems.length) {
return;
}
if (toIndex < 0 || toIndex >= this.panelItems.length) {
return;
}
const [panelItem] = this.panelItems.splice(fromIndex, 1);
this.panelItems.splice(toIndex, 0, panelItem);
this.panelview.movePanel(from, to);
}
resizePanel(panel: ViewletPanel, size: number): void {
this.panelview.resizePanel(panel, size);
}
getPanelSize(panel: ViewletPanel): number {
return this.panelview.getPanelSize(panel);
}
protected updateViewHeaders(): void {
if (this.isSingleView()) {
this.panelItems[0].panel.setExpanded(true);
this.panelItems[0].panel.headerVisible = false;
} else {
this.panelItems.forEach(i => i.panel.headerVisible = true);
}
}
protected isSingleView(): boolean {
return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1;
}
dispose(): void {
super.dispose();
this.panelItems.forEach(i => i.disposable.dispose());
this.panelview.dispose();
}
}

View File

@@ -0,0 +1,451 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/views';
import Event, { Emitter } from 'vs/base/common/event';
import { IDisposable, dispose, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import * as DOM from 'vs/base/browser/dom';
import { Builder, $ } from 'vs/base/browser/builder';
import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions';
import { IMessageService } from 'vs/platform/message/common/message';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IListService } from 'vs/platform/list/browser/listService';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
import { createActionItem, fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITree, IDataSource, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { ViewsRegistry } from 'vs/workbench/browser/parts/views/viewsRegistry';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { ViewsViewletPanel, IViewletViewOptions, IViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { TreeItemCollapsibleState, ITreeItem, ITreeViewDataProvider, TreeViewItemHandleArg } from 'vs/workbench/common/views';
export class TreeView extends ViewsViewletPanel {
private menus: Menus;
private viewFocusContext: IContextKey<boolean>;
private activated: boolean = false;
private treeInputPromise: TPromise<void>;
private dataProviderElementChangeListener: IDisposable;
constructor(
private options: IViewletViewOptions,
@IMessageService private messageService: IMessageService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IListService private listService: IListService,
@IThemeService private themeService: IThemeService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IExtensionService private extensionService: IExtensionService,
@ICommandService private commandService: ICommandService
) {
super({ ...(options as IViewOptions), ariaHeaderLabel: options.name }, keybindingService, contextMenuService);
this.menus = this.instantiationService.createInstance(Menus, this.id);
this.viewFocusContext = this.contextKeyService.createKey<boolean>(this.id, void 0);
this.menus.onDidChangeTitle(() => this.updateActions(), this, this.disposables);
this.themeService.onThemeChange(() => this.tree.refresh() /* soft refresh */, this, this.disposables);
if (options.expanded) {
this.activate();
}
}
public renderBody(container: HTMLElement): void {
this.treeContainer = super.renderViewTree(container);
DOM.addClass(this.treeContainer, 'tree-explorer-viewlet-tree-view');
this.tree = this.createViewer($(this.treeContainer));
this.setInput();
}
setExpanded(expanded: boolean): void {
super.setExpanded(expanded);
if (expanded) {
this.activate();
}
}
private activate() {
if (!this.activated && this.extensionService) {
this.extensionService.activateByEvent(`onView:${this.id}`);
this.activated = true;
this.setInput();
}
}
public createViewer(container: Builder): ITree {
const dataSource = this.instantiationService.createInstance(TreeDataSource, this.id);
const renderer = this.instantiationService.createInstance(TreeRenderer);
const controller = this.instantiationService.createInstance(TreeController, this.id, this.menus);
const tree = new Tree(container.getHTMLElement(), {
dataSource,
renderer,
controller
}, {
keyboardSupport: false
});
this.disposables.push(attachListStyler(tree, this.themeService));
this.disposables.push(this.listService.register(tree, [this.viewFocusContext]));
tree.addListener('selection', (event: any) => this.onSelection());
return tree;
}
getActions(): IAction[] {
return [...this.menus.getTitleActions()];
}
getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
}
getActionItem(action: IAction): IActionItem {
return createActionItem(action, this.keybindingService, this.messageService);
}
public setVisible(visible: boolean): TPromise<void> {
return super.setVisible(visible);
}
private setInput(): TPromise<void> {
if (this.tree) {
if (!this.treeInputPromise) {
if (this.listenToDataProvider()) {
this.treeInputPromise = this.tree.setInput(new Root());
} else {
this.treeInputPromise = new TPromise<void>((c, e) => {
this.disposables.push(ViewsRegistry.onTreeViewDataProviderRegistered(id => {
if (this.id === id) {
if (this.listenToDataProvider()) {
this.tree.setInput(new Root()).then(() => c(null));
}
}
}));
});
}
}
return this.treeInputPromise;
}
return TPromise.as(null);
}
private listenToDataProvider(): boolean {
let dataProvider = ViewsRegistry.getTreeViewDataProvider(this.id);
if (dataProvider) {
if (this.dataProviderElementChangeListener) {
this.dataProviderElementChangeListener.dispose();
}
this.dataProviderElementChangeListener = dataProvider.onDidChange(element => this.refresh(element));
const disposable = dataProvider.onDispose(() => {
this.dataProviderElementChangeListener.dispose();
this.tree.setInput(new Root());
disposable.dispose();
});
return true;
}
return false;
}
public getOptimalWidth(): number {
const parentNode = this.tree.getHTMLElement();
const childNodes = [].slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
return DOM.getLargestChildWidth(parentNode, childNodes);
}
private onSelection(): void {
const selection: ITreeItem = this.tree.getSelection()[0];
if (selection) {
if (selection.command) {
this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || []));
}
}
}
private refresh(elements: ITreeItem[]): void {
elements = elements ? elements : [this.tree.getInput()];
for (const element of elements) {
element.children = null;
this.tree.refresh(element);
}
}
dispose(): void {
dispose(this.disposables);
if (this.dataProviderElementChangeListener) {
this.dataProviderElementChangeListener.dispose();
}
dispose(this.disposables);
super.dispose();
}
}
class Root implements ITreeItem {
label = 'root';
handle = -1;
collapsibleState = TreeItemCollapsibleState.Expanded;
}
class TreeDataSource implements IDataSource {
constructor(
private id: string,
@IProgressService private progressService: IProgressService
) {
}
public getId(tree: ITree, node: ITreeItem): string {
return '' + node.handle;
}
public hasChildren(tree: ITree, node: ITreeItem): boolean {
if (!this.getDataProvider()) {
return false;
}
return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded;
}
public getChildren(tree: ITree, node: ITreeItem): TPromise<any[]> {
if (node.children) {
return TPromise.as(node.children);
}
const dataProvider = this.getDataProvider();
if (dataProvider) {
const promise = node instanceof Root ? dataProvider.getElements() : dataProvider.getChildren(node);
this.progressService.showWhile(promise, 100);
return promise.then(children => {
node.children = children;
return children;
});
}
return TPromise.as(null);
}
public shouldAutoexpand(tree: ITree, node: ITreeItem): boolean {
return node.collapsibleState === TreeItemCollapsibleState.Expanded;
}
public getParent(tree: ITree, node: any): TPromise<any> {
return TPromise.as(null);
}
private getDataProvider(): ITreeViewDataProvider {
return ViewsRegistry.getTreeViewDataProvider(this.id);
}
}
interface ITreeExplorerTemplateData {
icon: Builder;
label: Builder;
}
class TreeRenderer implements IRenderer {
private static ITEM_HEIGHT = 22;
private static TREE_TEMPLATE_ID = 'treeExplorer';
constructor( @IThemeService private themeService: IThemeService) {
}
public getHeight(tree: ITree, element: any): number {
return TreeRenderer.ITEM_HEIGHT;
}
public getTemplateId(tree: ITree, element: any): string {
return TreeRenderer.TREE_TEMPLATE_ID;
}
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData {
const el = $(container);
const item = $('.custom-view-tree-node-item');
item.appendTo(el);
const icon = $('.custom-view-tree-node-item-icon').appendTo(item);
const label = $('.custom-view-tree-node-item-label').appendTo(item);
const link = $('a.label').appendTo(label);
return { label: link, icon };
}
public renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void {
templateData.label.text(node.label).title(node.label);
const theme = this.themeService.getTheme();
const icon = theme.type === LIGHT ? node.icon : node.iconDark;
if (icon) {
templateData.icon.getHTMLElement().style.backgroundImage = `url('${icon}')`;
DOM.addClass(templateData.icon.getHTMLElement(), 'custom-view-tree-node-item-icon');
} else {
templateData.icon.getHTMLElement().style.backgroundImage = '';
DOM.removeClass(templateData.icon.getHTMLElement(), 'custom-view-tree-node-item-icon');
}
}
public disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void {
}
}
class TreeController extends DefaultController {
constructor(
private treeViewId: string,
private menus: Menus,
@IContextMenuService private contextMenuService: IContextMenuService,
@IKeybindingService private _keybindingService: IKeybindingService
) {
super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false });
}
public onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean {
event.preventDefault();
event.stopPropagation();
tree.setFocus(node);
const actions = this.menus.getResourceContextActions(node);
if (!actions.length) {
return true;
}
const anchor = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => {
return TPromise.as(actions);
},
getActionItem: (action) => {
const keybinding = this._keybindingService.lookupKeybinding(action.id);
if (keybinding) {
return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel() });
}
return null;
},
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
tree.DOMFocus();
}
},
getActionsContext: () => (<TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }),
actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection())
});
return true;
}
}
class MultipleSelectionActionRunner extends ActionRunner {
constructor(private getSelectedResources: () => any[]) {
super();
}
runAction(action: IAction, context: any): TPromise<any> {
if (action instanceof MenuItemAction) {
const selection = this.getSelectedResources();
const filteredSelection = selection.filter(s => s !== context);
if (selection.length === filteredSelection.length || selection.length === 1) {
return action.run(context);
}
return action.run(context, ...filteredSelection);
}
return super.runAction(action, context);
}
}
class Menus implements IDisposable {
private disposables: IDisposable[] = [];
private titleDisposable: IDisposable = EmptyDisposable;
private titleActions: IAction[] = [];
private titleSecondaryActions: IAction[] = [];
private _onDidChangeTitle = new Emitter<void>();
get onDidChangeTitle(): Event<void> { return this._onDidChangeTitle.event; }
constructor(
private id: string,
@IContextKeyService private contextKeyService: IContextKeyService,
@IMenuService private menuService: IMenuService
) {
if (this.titleDisposable) {
this.titleDisposable.dispose();
this.titleDisposable = EmptyDisposable;
}
const _contextKeyService = this.contextKeyService.createScoped();
_contextKeyService.createKey('view', id);
const titleMenu = this.menuService.createMenu(MenuId.ViewTitle, _contextKeyService);
const updateActions = () => {
this.titleActions = [];
this.titleSecondaryActions = [];
fillInActions(titleMenu, null, { primary: this.titleActions, secondary: this.titleSecondaryActions });
this._onDidChangeTitle.fire();
};
const listener = titleMenu.onDidChange(updateActions);
updateActions();
this.titleDisposable = toDisposable(() => {
listener.dispose();
titleMenu.dispose();
_contextKeyService.dispose();
this.titleActions = [];
this.titleSecondaryActions = [];
});
}
getTitleActions(): IAction[] {
return this.titleActions;
}
getTitleSecondaryActions(): IAction[] {
return this.titleSecondaryActions;
}
getResourceContextActions(element: ITreeItem): IAction[] {
return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary;
}
private getActions(menuId: MenuId, context: { key: string, value: string }): { primary: IAction[]; secondary: IAction[]; } {
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.id);
contextKeyService.createKey(context.key, context.value);
const menu = this.menuService.createMenu(menuId, contextKeyService);
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };
fillInActions(menu, { shouldForwardArgs: true }, result);
menu.dispose();
contextKeyService.dispose();
return result;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Event, { Emitter } from 'vs/base/common/event';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ITreeViewDataProvider } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
export class ViewLocation {
static readonly Explorer = new ViewLocation('explorer');
static readonly Debug = new ViewLocation('debug');
static readonly Extensions = new ViewLocation('extensions');
static readonly SCM = new ViewLocation('scm');
constructor(private _id: string) {
}
get id(): string {
return this._id;
}
static getContributedViewLocation(value: string): ViewLocation {
switch (value) {
case ViewLocation.Explorer.id: return ViewLocation.Explorer;
case ViewLocation.Debug.id: return ViewLocation.Debug;
}
return void 0;
}
}
export interface IViewDescriptor {
readonly id: string;
readonly name: string;
readonly location: ViewLocation;
// TODO do we really need this?!
readonly ctor: any;
readonly when?: ContextKeyExpr;
readonly order?: number;
readonly size?: number;
readonly collapsed?: boolean;
readonly canToggleVisibility?: boolean;
}
export interface IViewsRegistry {
readonly onViewsRegistered: Event<IViewDescriptor[]>;
readonly onViewsDeregistered: Event<IViewDescriptor[]>;
readonly onTreeViewDataProviderRegistered: Event<string>;
registerViews(views: IViewDescriptor[]): void;
deregisterViews(ids: string[], location: ViewLocation): void;
registerTreeViewDataProvider(id: string, factory: ITreeViewDataProvider): void;
deregisterTreeViewDataProviders(): void;
getViews(loc: ViewLocation): IViewDescriptor[];
getTreeViewDataProvider(id: string): ITreeViewDataProvider;
}
export const ViewsRegistry: IViewsRegistry = new class {
private _onViewsRegistered: Emitter<IViewDescriptor[]> = new Emitter<IViewDescriptor[]>();
readonly onViewsRegistered: Event<IViewDescriptor[]> = this._onViewsRegistered.event;
private _onViewsDeregistered: Emitter<IViewDescriptor[]> = new Emitter<IViewDescriptor[]>();
readonly onViewsDeregistered: Event<IViewDescriptor[]> = this._onViewsDeregistered.event;
private _onTreeViewDataProviderRegistered: Emitter<string> = new Emitter<string>();
readonly onTreeViewDataProviderRegistered: Event<string> = this._onTreeViewDataProviderRegistered.event;
private _views: Map<ViewLocation, IViewDescriptor[]> = new Map<ViewLocation, IViewDescriptor[]>();
private _treeViewDataPoviders: Map<string, ITreeViewDataProvider> = new Map<string, ITreeViewDataProvider>();
registerViews(viewDescriptors: IViewDescriptor[]): void {
if (viewDescriptors.length) {
for (const viewDescriptor of viewDescriptors) {
let views = this._views.get(viewDescriptor.location);
if (!views) {
views = [];
this._views.set(viewDescriptor.location, views);
}
if (views.some(v => v.id === viewDescriptor.id)) {
throw new Error(localize('duplicateId', "A view with id `{0}` is already registered in the location `{1}`", viewDescriptor.id, viewDescriptor.location.id));
}
views.push(viewDescriptor);
}
this._onViewsRegistered.fire(viewDescriptors);
}
}
deregisterViews(ids: string[], location: ViewLocation): void {
const views = this._views.get(location);
if (!views) {
return;
}
const viewsToDeregister = views.filter(view => ids.indexOf(view.id) !== -1);
if (viewsToDeregister.length) {
this._views.set(location, views.filter(view => ids.indexOf(view.id) === -1));
}
this._onViewsDeregistered.fire(viewsToDeregister);
}
registerTreeViewDataProvider<T>(id: string, factory: ITreeViewDataProvider) {
if (!this.isDataProviderRegistered(id)) {
// TODO: throw error
}
this._treeViewDataPoviders.set(id, factory);
this._onTreeViewDataProviderRegistered.fire(id);
}
deregisterTreeViewDataProviders(): void {
this._treeViewDataPoviders.clear();
}
getViews(loc: ViewLocation): IViewDescriptor[] {
return this._views.get(loc) || [];
}
getTreeViewDataProvider(id: string): ITreeViewDataProvider {
return this._treeViewDataPoviders.get(id);
}
private isDataProviderRegistered(id: string): boolean {
let registered = false;
this._views.forEach(views => registered = registered || views.some(view => view.id === id));
return registered;
}
};

View File

@@ -0,0 +1,653 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import * as errors from 'vs/base/common/errors';
import * as DOM from 'vs/base/browser/dom';
import { $, Dimension, Builder } from 'vs/base/browser/builder';
import { Scope } from 'vs/workbench/common/memento';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { firstIndex } from 'vs/base/common/arrays';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ViewsRegistry, ViewLocation, IViewDescriptor } from 'vs/workbench/browser/parts/views/viewsRegistry';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IPanelOptions } from 'vs/base/browser/ui/splitview/panelview';
export interface IViewOptions extends IPanelOptions {
id: string;
name: string;
actionRunner: IActionRunner;
}
export interface IViewConstructorSignature<T extends ViewsViewletPanel> {
new(options: IViewOptions, ...services: { _serviceBrand: any; }[]): T;
}
export abstract class ViewsViewletPanel extends ViewletPanel {
readonly id: string;
readonly name: string;
protected treeContainer: HTMLElement;
// TODO@sandeep why is tree here? isn't this coming only from TreeView
protected tree: ITree;
protected isDisposed: boolean;
private _isVisible: boolean;
private dragHandler: DelayedDragHandler;
constructor(
options: IViewOptions,
protected keybindingService: IKeybindingService,
protected contextMenuService: IContextMenuService
) {
super(options.name, options, keybindingService, contextMenuService);
this.id = options.id;
this.name = options.name;
this._expanded = options.expanded;
}
setExpanded(expanded: boolean): void {
this.updateTreeVisibility(this.tree, expanded);
super.setExpanded(expanded);
}
protected renderHeader(container: HTMLElement): void {
super.renderHeader(container);
// Expand on drag over
this.dragHandler = new DelayedDragHandler(container, () => this.setExpanded(true));
}
protected renderViewTree(container: HTMLElement): HTMLElement {
const treeContainer = document.createElement('div');
container.appendChild(treeContainer);
return treeContainer;
}
getViewer(): ITree {
return this.tree;
}
isVisible(): boolean {
return this._isVisible;
}
setVisible(visible: boolean): TPromise<void> {
if (this._isVisible !== visible) {
this._isVisible = visible;
this.updateTreeVisibility(this.tree, visible && this.isExpanded());
}
return TPromise.as(null);
}
focus(): void {
super.focus();
this.focusTree();
}
protected reveal(element: any, relativeTop?: number): TPromise<void> {
if (!this.tree) {
return TPromise.as(null); // return early if viewlet has not yet been created
}
return this.tree.reveal(element, relativeTop);
}
layoutBody(size: number): void {
if (this.tree) {
this.treeContainer.style.height = size + 'px';
this.tree.layout(size);
}
}
getActions(): IAction[] {
return [];
}
getSecondaryActions(): IAction[] {
return [];
}
getActionItem(action: IAction): IActionItem {
return null;
}
getActionsContext(): any {
return undefined;
}
getOptimalWidth(): number {
return 0;
}
create(): TPromise<void> {
return TPromise.as(null);
}
shutdown(): void {
// Subclass to implement
}
dispose(): void {
this.isDisposed = true;
this.treeContainer = null;
if (this.tree) {
this.tree.dispose();
}
if (this.dragHandler) {
this.dragHandler.dispose();
}
super.dispose();
}
private updateTreeVisibility(tree: ITree, isVisible: boolean): void {
if (!tree) {
return;
}
if (isVisible) {
$(tree.getHTMLElement()).show();
} else {
$(tree.getHTMLElement()).hide(); // make sure the tree goes out of the tabindex world by hiding it
}
if (isVisible) {
tree.onVisible();
} else {
tree.onHidden();
}
}
private focusTree(): void {
if (!this.tree) {
return; // return early if viewlet has not yet been created
}
// Make sure the current selected element is revealed
const selection = this.tree.getSelection();
if (selection.length > 0) {
this.reveal(selection[0], 0.5).done(null, errors.onUnexpectedError);
}
// Pass Focus to Viewer
this.tree.DOMFocus();
}
}
export interface IViewletViewOptions extends IViewOptions {
viewletSettings: object;
}
export interface IViewState {
collapsed: boolean;
size: number | undefined;
isHidden: boolean;
order: number;
}
export class ViewsViewlet extends PanelViewlet {
private viewHeaderContextMenuListeners: IDisposable[] = [];
private viewletSettings: object;
private readonly viewsContextKeys: Set<string> = new Set<string>();
private viewsViewletPanels: ViewsViewletPanel[] = [];
private didLayout = false;
protected viewsStates: Map<string, IViewState> = new Map<string, IViewState>();
private areExtensionsReady: boolean = false;
constructor(
id: string,
private location: ViewLocation,
private showHeaderInTitleWhenSingleView: boolean,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService protected storageService: IStorageService,
@IInstantiationService protected instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextKeyService protected contextKeyService: IContextKeyService,
@IContextMenuService protected contextMenuService: IContextMenuService,
@IExtensionService protected extensionService: IExtensionService
) {
super(id, { showHeaderInTitleWhenSingleView, dnd: true }, telemetryService, themeService);
this.viewletSettings = this.getMemento(storageService, Scope.WORKSPACE);
}
async create(parent: Builder): TPromise<void> {
await super.create(parent);
this._register(this.onDidSashChange(() => this.updateAllViewsSizes()));
this._register(ViewsRegistry.onViewsRegistered(this.onViewsRegistered, this));
this._register(ViewsRegistry.onViewsDeregistered(this.onViewsDeregistered, this));
this._register(this.contextKeyService.onDidChangeContext(keys => this.onContextChanged(keys)));
// Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609
this.extensionService.onReady().then(() => {
this.areExtensionsReady = true;
this.updateHeaders();
});
this.onViewsRegistered(ViewsRegistry.getViews(this.location));
this.focus();
}
getContextMenuActions(): IAction[] {
return this.getViewDescriptorsFromRegistry(true)
.filter(viewDescriptor => viewDescriptor.canToggleVisibility && this.contextKeyService.contextMatchesRules(viewDescriptor.when))
.map(viewDescriptor => (<IAction>{
id: `${viewDescriptor.id}.toggleVisibility`,
label: viewDescriptor.name,
checked: this.isCurrentlyVisible(viewDescriptor),
enabled: true,
run: () => this.toggleViewVisibility(viewDescriptor.id)
}));
}
setVisible(visible: boolean): TPromise<void> {
return super.setVisible(visible)
.then(() => TPromise.join(this.viewsViewletPanels.filter(view => view.isVisible() !== visible)
.map((view) => view.setVisible(visible))))
.then(() => void 0);
}
layout(dimension: Dimension): void {
super.layout(dimension);
if (!this.didLayout) {
this.didLayout = true;
this._resizePanels();
}
this.updateAllViewsSizes();
}
getOptimalWidth(): number {
const additionalMargin = 16;
const optimalWidth = Math.max(...this.viewsViewletPanels.map(view => view.getOptimalWidth() || 0));
return optimalWidth + additionalMargin;
}
shutdown(): void {
this.viewsViewletPanels.forEach((view) => view.shutdown());
super.shutdown();
}
toggleViewVisibility(id: string, visible?: boolean): void {
const view = this.getView(id);
let viewState = this.viewsStates.get(id);
if ((visible === true && view) || (visible === false && !view)) {
return;
}
if (view) {
viewState = viewState || this.createViewState(view);
viewState.isHidden = true;
} else {
viewState = viewState || { collapsed: true, size: void 0, isHidden: false, order: void 0 };
viewState.isHidden = false;
}
this.viewsStates.set(id, viewState);
this.updateViews();
}
private onViewsRegistered(views: IViewDescriptor[]): TPromise<ViewsViewletPanel[]> {
this.viewsContextKeys.clear();
for (const viewDescriptor of this.getViewDescriptorsFromRegistry()) {
if (viewDescriptor.when) {
for (const key of viewDescriptor.when.keys()) {
this.viewsContextKeys.add(key);
}
}
}
return this.updateViews();
}
private onViewsDeregistered(views: IViewDescriptor[]): TPromise<ViewsViewletPanel[]> {
return this.updateViews(views);
}
private onContextChanged(keys: string[]): void {
if (!keys) {
return;
}
let hasToUpdate: boolean = false;
for (const key of keys) {
if (this.viewsContextKeys.has(key)) {
hasToUpdate = true;
break;
}
}
if (hasToUpdate) {
this.updateViews();
}
}
protected updateViews(unregisteredViews: IViewDescriptor[] = []): TPromise<ViewsViewletPanel[]> {
const registeredViews = this.getViewDescriptorsFromRegistry();
const [visible, toAdd, toRemove] = registeredViews.reduce<[IViewDescriptor[], IViewDescriptor[], IViewDescriptor[]]>((result, viewDescriptor) => {
const isCurrentlyVisible = this.isCurrentlyVisible(viewDescriptor);
const canBeVisible = this.canBeVisible(viewDescriptor);
if (canBeVisible) {
result[0].push(viewDescriptor);
}
if (!isCurrentlyVisible && canBeVisible) {
result[1].push(viewDescriptor);
}
if (isCurrentlyVisible && !canBeVisible) {
result[2].push(viewDescriptor);
}
return result;
}, [[], [], []]);
toRemove.push(...unregisteredViews.filter(viewDescriptor => this.isCurrentlyVisible(viewDescriptor)));
const toCreate: ViewsViewletPanel[] = [];
if (toAdd.length || toRemove.length) {
const panels = [...this.viewsViewletPanels];
for (const view of panels) {
let viewState = this.viewsStates.get(view.id);
if (!viewState || typeof viewState.size === 'undefined' || !view.isExpanded() !== viewState.collapsed) {
viewState = this.updateViewStateSize(view);
this.viewsStates.set(view.id, viewState);
}
}
if (toRemove.length) {
for (const viewDescriptor of toRemove) {
let view = this.getView(viewDescriptor.id);
const viewState = this.updateViewStateSize(view);
this.viewsStates.set(view.id, viewState);
this.removePanel(view);
this.viewsViewletPanels.splice(this.viewsViewletPanels.indexOf(view), 1);
}
}
for (const viewDescriptor of toAdd) {
let viewState = this.viewsStates.get(viewDescriptor.id);
let index = visible.indexOf(viewDescriptor);
const view = this.createView(viewDescriptor,
{
id: viewDescriptor.id,
name: viewDescriptor.name,
actionRunner: this.getActionRunner(),
expanded: !(viewState ? viewState.collapsed : viewDescriptor.collapsed),
viewletSettings: this.viewletSettings
});
toCreate.push(view);
const size = (viewState && viewState.size) || viewDescriptor.size || 200;
this.addPanel(view, size, index);
this.viewsViewletPanels.splice(index, 0, view);
this.viewsStates.set(view.id, this.updateViewStateSize(view));
}
return TPromise.join(toCreate.map(view => view.create()))
.then(() => this.onViewsUpdated())
.then(() => this._resizePanels())
.then(() => toCreate);
}
return TPromise.as([]);
}
private updateAllViewsSizes(): void {
for (const view of this.viewsViewletPanels) {
let viewState = this.updateViewStateSize(view);
this.viewsStates.set(view.id, viewState);
}
}
private _resizePanels(): void {
if (!this.didLayout) {
return;
}
for (const panel of this.viewsViewletPanels) {
const viewState = this.viewsStates.get(panel.id);
const size = (viewState && viewState.size) || 200;
this.resizePanel(panel, size);
}
}
movePanel(from: ViewletPanel, to: ViewletPanel): void {
const fromIndex = firstIndex(this.viewsViewletPanels, panel => panel === from);
const toIndex = firstIndex(this.viewsViewletPanels, panel => panel === to);
if (fromIndex < 0 || fromIndex >= this.viewsViewletPanels.length) {
return;
}
if (toIndex < 0 || toIndex >= this.viewsViewletPanels.length) {
return;
}
super.movePanel(from, to);
const [panel] = this.viewsViewletPanels.splice(fromIndex, 1);
this.viewsViewletPanels.splice(toIndex, 0, panel);
for (let order = 0; order < this.viewsViewletPanels.length; order++) {
const view = this.viewsStates.get(this.viewsViewletPanels[order].id);
if (!view) {
continue;
}
view.order = order;
}
}
protected getDefaultViewSize(): number | undefined {
return undefined;
}
private isCurrentlyVisible(viewDescriptor: IViewDescriptor): boolean {
return !!this.getView(viewDescriptor.id);
}
private canBeVisible(viewDescriptor: IViewDescriptor): boolean {
const viewstate = this.viewsStates.get(viewDescriptor.id);
if (viewDescriptor.canToggleVisibility && viewstate && viewstate.isHidden) {
return false;
}
return this.contextKeyService.contextMatchesRules(viewDescriptor.when);
}
private onViewsUpdated(): TPromise<void> {
this.viewHeaderContextMenuListeners = dispose(this.viewHeaderContextMenuListeners);
for (const viewDescriptor of this.getViewDescriptorsFromRegistry()) {
const view = this.getView(viewDescriptor.id);
if (view) {
this.viewHeaderContextMenuListeners.push(DOM.addDisposableListener(view.draggableElement, DOM.EventType.CONTEXT_MENU, e => {
e.stopPropagation();
e.preventDefault();
if (viewDescriptor.canToggleVisibility) {
this.onContextMenu(new StandardMouseEvent(e), view);
}
}));
}
}
return this.setVisible(this.isVisible());
}
private updateHeaders(): void {
if (this.viewsViewletPanels.length) {
this.updateTitleArea();
this.updateViewHeaders();
}
}
private onContextMenu(event: StandardMouseEvent, view: ViewsViewletPanel): void {
event.stopPropagation();
event.preventDefault();
let anchor: { x: number, y: number } = { x: event.posx, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as([<IAction>{
id: `${view.id}.removeView`,
label: nls.localize('hideView', "Hide from Side Bar"),
enabled: true,
run: () => this.toggleViewVisibility(view.id)
}]),
});
}
protected isSingleView(): boolean {
if (!this.showHeaderInTitleWhenSingleView) {
return false;
}
if (this.getViewDescriptorsFromRegistry().length === 0) {
return false;
}
if (this.length > 1) {
return false;
}
// Check in cache so that view do not jump. See #29609
if (ViewLocation.getContributedViewLocation(this.location.id) && !this.areExtensionsReady) {
let visibleViewsCount = 0;
this.viewsStates.forEach((viewState, id) => {
if (!viewState.isHidden) {
visibleViewsCount++;
}
});
return visibleViewsCount === 1;
}
return super.isSingleView();
}
protected getViewDescriptorsFromRegistry(defaultOrder: boolean = false): IViewDescriptor[] {
return ViewsRegistry.getViews(this.location)
.sort((a, b) => {
const viewStateA = this.viewsStates.get(a.id);
const viewStateB = this.viewsStates.get(b.id);
const orderA = !defaultOrder && viewStateA ? viewStateA.order : a.order;
const orderB = !defaultOrder && viewStateB ? viewStateB.order : b.order;
if (orderB === void 0 || orderB === null) {
return -1;
}
if (orderA === void 0 || orderA === null) {
return 1;
}
return orderA - orderB;
});
}
protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewsViewletPanel {
return this.instantiationService.createInstance(viewDescriptor.ctor, options);
}
protected get views(): ViewsViewletPanel[] {
return this.viewsViewletPanels;
}
protected getView(id: string): ViewsViewletPanel {
return this.viewsViewletPanels.filter(view => view.id === id)[0];
}
private updateViewStateSize(view: ViewsViewletPanel): IViewState {
const currentState = this.viewsStates.get(view.id);
const newViewState = this.createViewState(view);
return currentState ? { ...currentState, collapsed: newViewState.collapsed, size: newViewState.size } : newViewState;
}
protected createViewState(view: ViewsViewletPanel): IViewState {
return {
collapsed: !view.isExpanded(),
size: this.getPanelSize(view),
isHidden: false,
order: this.viewsViewletPanels.indexOf(view)
};
}
}
export class PersistentViewsViewlet extends ViewsViewlet {
constructor(
id: string,
location: ViewLocation,
private viewletStateStorageId: string,
showHeaderInTitleWhenSingleView: boolean,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService
) {
super(id, location, showHeaderInTitleWhenSingleView, telemetryService, storageService, instantiationService, themeService, contextKeyService, contextMenuService, extensionService);
}
create(parent: Builder): TPromise<void> {
this.loadViewsStates();
return super.create(parent);
}
shutdown(): void {
this.saveViewsStates();
super.shutdown();
}
protected saveViewsStates(): void {
const viewsStates = {};
const registeredViewDescriptors = this.getViewDescriptorsFromRegistry();
this.viewsStates.forEach((viewState, id) => {
const view = this.getView(id);
if (view) {
viewsStates[id] = this.createViewState(view);
} else {
const viewDescriptor = registeredViewDescriptors.filter(v => v.id === id)[0];
if (viewDescriptor) {
viewsStates[id] = viewState;
}
}
});
this.storageService.store(this.viewletStateStorageId, JSON.stringify(viewsStates), this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL);
}
protected loadViewsStates(): void {
const viewsStates = JSON.parse(this.storageService.get(this.viewletStateStorageId, this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL, '{}'));
Object.keys(viewsStates).forEach(id => this.viewsStates.set(id, <IViewState>viewsStates[id]));
}
}