Merge pull request #8059 from Microsoft/joh/menus

refine menu contributions
This commit is contained in:
Johannes Rieken 2016-06-24 09:46:59 +02:00 committed by GitHub
commit 8b60f07f69
27 changed files with 840 additions and 585 deletions

View file

@ -4,7 +4,7 @@
"description": "Markdown for VS Code",
"version": "0.2.0",
"publisher": "Microsoft",
"aiKey":"AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"engines": {
"vscode": "^1.0.0"
},
@ -44,21 +44,20 @@
"commands": [
{
"command": "markdown.showPreview",
"title": "%markdown.openPreview%",
"category": "%markdown.category%",
"where": ["explorer/context"],
"when": "markdown"
},
{
"command": "markdown.showPreview",
"title": "%markdown.previewMarkdown.title%",
"title": "%markdown.preview.title%",
"category": "%markdown.category%",
"icon": {
"light": "./media/Preview.svg",
"dark": "./media/Preview_inverse.svg"
},
"where": ["editor/primary"],
"when": "markdown"
}
},
{
"command": "markdown.showPreviewToSide",
"title": "%markdown.previewSide.title%",
"icon": {
"light": "./media/Preview.svg",
"dark": "./media/Preview_inverse.svg"
}
},
{
"command": "markdown.showSource",
@ -67,17 +66,28 @@
"icon": {
"light": "./media/ViewSource.svg",
"dark": "./media/ViewSource_inverse.svg"
},
"where": ["editor/primary"],
"when": { "scheme": "markdown" }
},
{
"command": "markdown.showPreviewToSide",
"title": "%markdown.previewMarkdownSide.title%",
"where": "editor/secondary",
"when": "markdown"
}
}
],
"menus": {
"editor/title": [
{
"when": "resourceLangId == markdown",
"command": "markdown.showPreview",
"alt": "markdown.showPreviewToSide"
},
{
"when": "resourceScheme == markdown",
"command": "markdown.showSource"
}
],
"explorer/context": [
{
"when": "resourceLangId == markdown",
"command": "markdown.showPreview"
}
]
},
"keybindings": [
{
"command": "markdown.showPreview",
@ -90,17 +100,19 @@
"mac": "cmd+k v"
}
],
"snippets": [{
"language": "markdown",
"path": "./snippets/markdown.json"
}],
"snippets": [
{
"language": "markdown",
"path": "./snippets/markdown.json"
}
],
"configuration": {
"type": "object",
"title": "Markdown preview configuration",
"properties": {
"markdown.styles": {
"type": "array",
"default" : null,
"default": null,
"description": "A list of URLs or local paths to CSS style sheets to use from the markdown preview."
}
}

View file

@ -1,7 +1,6 @@
{
"markdown.category" : "Markdown",
"markdown.openPreview" : "Open Preview",
"markdown.previewMarkdown.title" : "Show Preview",
"markdown.previewMarkdownSide.title" : "Open Preview to the Side",
"markdown.preview.title" : "Open Preview",
"markdown.previewSide.title" : "Open Preview to the Side",
"markdown.showSource.title" : "Show Source"
}

View file

@ -222,9 +222,9 @@ export interface IActionItemOptions {
export class ActionItem extends BaseActionItem {
$e: Builder;
protected $e: Builder;
protected options: IActionItemOptions;
private cssClass: string;
private options: IActionItemOptions;
constructor(context: any, action: IAction, options: IActionItemOptions = {}) {
super(context, action);

View file

@ -159,6 +159,22 @@ export function filterEvent<T>(event: Event<T>, filter: (e:T)=>boolean): Event<T
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function debounceEvent<I, O>(event: Event<I>, merger: (last: O, event: I) => O, delay: number = 100): Event<O> {
let output: O;
let handle: number;
return (listener, thisArgs?, disposables?) => event(cur => {
output = merger(output, cur);
clearTimeout(handle);
handle = setTimeout(() => {
listener.call(thisArgs, output);
output = undefined;
}, delay);
}, thisArgs, disposables);
}
enum EventDelayerState {
Idle,
Running

View file

@ -66,6 +66,8 @@ export class MockCodeEditor extends CommonCodeEditor {
export class MockScopeLocation implements IKeybindingScopeLocation {
setAttribute(attr:string, value:string): void { }
removeAttribute(attr:string): void { }
hasAttribute(attr: string): boolean { return false; }
getAttribute(attr: string): string { return; }
}
export function withMockCodeEditor(text:string[], options:editorCommon.ICodeEditorWidgetCreationOptions, callback:(editor:MockCodeEditor, cursor:Cursor)=>void): void {

View file

@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import {localize} from 'vs/nls';
import Event, {Emitter} from 'vs/base/common/event';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import {IKeybindingService, KbExpr} from 'vs/platform/keybinding/common/keybindingService';
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
import {MenuId, MenuItem, IMenuService} from 'vs/platform/actions/common/actions';
import {ResourceContextKey} from 'vs/platform/actions/common/resourceContextKey';
import {Action, IAction} from 'vs/base/common/actions';
import {BaseActionItem, ActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {domEvent} from 'vs/base/browser/event';
function fillInKbExprKeys(exp: KbExpr, set: { [k: string]: boolean }): void {
if (exp) {
const parts = exp.serialize().split(' && ');
for (let part of parts) {
const m = /^\w+/.exec(part);
if (m) {
set[m[0]] = true;
}
}
}
}
export class ActionBarContributor {
private _scope: HTMLElement;
private _onDidUpdate = new Emitter<this>();
private _disposables: IDisposable[] = [];
private _menuItems: MenuItem[] = [];
constructor(
scope: HTMLElement,
location: MenuId,
@IMenuService private _menuService: IMenuService,
@IKeybindingService private _keybindingService: IKeybindingService,
@IExtensionService private _extensionService: IExtensionService
) {
this._scope = scope;
this._extensionService.onReady().then(() => {
let menuItems = this._menuService.getMenuItems(location);
if (menuItems) {
let keysFilter: { [key: string]: boolean } = Object.create(null);
for (let item of menuItems) {
if (!item.command) {
//TODO@joh, warn? default command?
continue;
}
// keep menu items
this._menuItems.push(item);
fillInKbExprKeys(item.when, keysFilter);
}
this._keybindingService.onDidChangeContext(keys => {
for (let k of keys) {
if (keysFilter[k]) {
this._onDidUpdate.fire();
return;
}
}
}, undefined, this._disposables);
}
this._onDidUpdate.fire();
});
}
dispose() {
this._disposables = dispose(this._disposables);
}
get onDidUpdate(): Event<this> {
return this._onDidUpdate.event;
}
getActions(): IAction[] {
const result: IAction[] = [];
for (let item of this._menuItems) {
if (this._keybindingService.contextMatchesRules(this._scope, item.when)) {
result.push(new MenuItemAction(item,
this._keybindingService.getContextValue<URI>(this._scope, ResourceContextKey.Resource),
this._keybindingService));
}
}
return result;
}
getActionItem(action: IAction): BaseActionItem {
if (action instanceof MenuItemAction) {
return new MenuItemActionItem(action, this._keybindingService);
}
}
}
class MenuItemAction extends Action {
private static _getMenuItemId(item: MenuItem): string {
let result = item.command.id;
if (item.alt) {
result += `||${item.alt.id}`;
}
return result;
}
constructor(
private _item: MenuItem,
private _resource: URI,
@IKeybindingService private _keybindingService: IKeybindingService
) {
super(MenuItemAction._getMenuItemId(_item), _item.command.title);
}
get command() {
return this._item.command;
}
get altCommand() {
return this._item.alt;
}
get selectedCommand() {
return this.command;
}
run(alt: boolean) {
const {id} = alt && this._item.alt || this._item.command;
return this._keybindingService.executeCommand(id, this._resource);
}
}
class MenuItemActionItem extends ActionItem {
private _altKeyDown: boolean = false;
constructor(
action: MenuItemAction,
@IKeybindingService private _keybindingService: IKeybindingService
) {
super(undefined, action, { icon: !!action.command.iconClass, label: !action.command.iconClass });
}
private get command() {
const {command, altCommand} = <MenuItemAction>this._action;
return this._altKeyDown && altCommand || command;
}
onClick(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
(<MenuItemAction>this._action).run(this._altKeyDown).done(undefined, console.error);
}
render(container: HTMLElement): void {
super.render(container);
this._callOnDispose.push(domEvent(container, 'mousemove')(e => {
if (this._altKeyDown !== e.altKey) {
this._altKeyDown = e.altKey;
this._updateLabel();
this._updateTooltip();
this._updateClass();
}
}));
}
_updateLabel(): void {
if (this.options.label) {
this.$e.text(this.command.title);
}
}
_updateTooltip(): void {
const element = this.$e.getHTMLElement();
const keybinding = this._keybindingService.lookupKeybindings(this.command.id)[0];
const keybindingLabel = keybinding && this._keybindingService.getLabelFor(keybinding);
element.title = keybindingLabel
? localize('titleAndKb', "{0} ({1})", this.command.title, keybindingLabel)
: this.command.title;
}
_updateClass(): void {
if (this.options.icon) {
const element = this.$e.getHTMLElement();
const {iconClass} = this.command;
element.classList.add('icon', iconClass);
}
}
}

View file

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {values} from 'vs/base/common/collections';
import {KbExpr} from 'vs/platform/keybinding/common/keybindingService';
import {MenuId, CommandAction, MenuItem, IMenuService} from 'vs/platform/actions/common/actions';
export interface IDeclaredMenuItem {
command: string;
alt?: string;
when?: string;
}
export interface IMenuRegistry {
registerCommand(userCommand: CommandAction): boolean;
registerMenuItems(location: MenuId, items: IDeclaredMenuItem[]): void;
}
const _registry = new class {
commands: { [id: string]: CommandAction } = Object.create(null);
menuItems: { [loc: number]: IDeclaredMenuItem[] } = Object.create(null);
registerCommand(command: CommandAction): boolean {
const old = this.commands[command.id];
this.commands[command.id] = command;
return old !== void 0;
}
registerMenuItems(loc: MenuId, items: IDeclaredMenuItem[]): void {
let array = this.menuItems[loc];
if (!array) {
this.menuItems[loc] = items;
} else {
array.push(...items);
}
}
};
export const MenuRegistry: IMenuRegistry = _registry;
export class MenuService implements IMenuService {
serviceId;
getMenuItems(loc: MenuId): MenuItem[] {
const menuItems = _registry.menuItems[loc];
if (menuItems) {
return menuItems.map(item => {
const when = KbExpr.deserialize(item.when);
const command = _registry.commands[item.command];
const alt = _registry.commands[item.alt];
return { when, command, alt };
});
}
}
getCommandActions(): CommandAction[] {
return values(_registry.commands);
}
}

View file

@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {createCSSRule} from 'vs/base/browser/dom';
import {localize} from 'vs/nls';
import {join} from 'vs/base/common/paths';
import {IdGenerator} from 'vs/base/common/idGenerator';
import {IJSONSchema} from 'vs/base/common/jsonSchema';
import {forEach} from 'vs/base/common/collections';
import {IExtensionPointUser, IExtensionMessageCollector, ExtensionsRegistry} from 'vs/platform/extensions/common/extensionsRegistry';
import {IDeclaredMenuItem, MenuRegistry} from './menuService';
import {MenuId} from 'vs/platform/actions/common/actions';
namespace schema {
// --- menus contribution point
export function parseMenuId(value: string): MenuId {
switch (value) {
case 'editor/title': return MenuId.EditorTitle;
case 'explorer/context': return MenuId.ExplorerContext;
}
}
export function isValidMenuItems(menu: IDeclaredMenuItem[], collector: IExtensionMessageCollector): boolean {
if (!Array.isArray(menu)) {
collector.error(localize('requirearry', "menu items must be an arry"));
return false;
}
for (let item of menu) {
if (typeof item.command !== 'string') {
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
return false;
}
if (item.alt && typeof item.alt !== 'string') {
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt'));
return false;
}
if (item.when && typeof item.when !== 'string') {
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
return false;
}
}
return true;
}
const menuItem: IJSONSchema = {
type: 'object',
properties: {
command: {
description: localize('vscode.extension.contributes.menuItem.command', 'Identifier of the command to execute'),
type: 'string'
},
alt: {
description: localize('vscode.extension.contributes.menuItem.alt', 'Identifier of an alternative command to execute'),
type: 'string'
},
when: {
description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),
type: 'string'
}
}
};
export const menusContribtion: IJSONSchema = {
description: localize('vscode.extension.contributes.menus', "Contributes menu items to predefined locations"),
type: 'object',
properties: {
'editor/title': {
type: 'array',
items: menuItem
},
'explorer/context': {
type: 'array',
items: menuItem
}
}
};
// --- commands contribution point
export interface IUserFriendlyCommand {
command: string;
title: string;
category?: string;
icon?: IUserFriendlyIcon;
}
export type IUserFriendlyIcon = string | { light: string; dark: string; };
export function isValidCommand(command: IUserFriendlyCommand, collector: IExtensionMessageCollector): boolean {
if (!command) {
collector.error(localize('nonempty', "expected non-empty value."));
return false;
}
if (typeof command.command !== 'string') {
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
return false;
}
if (typeof command.title !== 'string') {
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title'));
return false;
}
if (command.category && typeof command.category !== 'string') {
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'category'));
return false;
}
if (!isValidIcon(command.icon, collector)) {
return false;
}
return true;
}
function isValidIcon(icon: IUserFriendlyIcon, collector: IExtensionMessageCollector): boolean {
if (typeof icon === 'undefined') {
return true;
}
if (typeof icon === 'string') {
return true;
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
return true;
}
collector.error(localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
return false;
}
const commandType: IJSONSchema = {
type: 'object',
properties: {
command: {
description: localize('vscode.extension.contributes.commandType.command', 'Identifier of the command to execute'),
type: 'string'
},
title: {
description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'),
type: 'string'
},
category: {
description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'),
type: 'string'
},
icon: {
description: localize('vscode.extension.contributes.commandType.icon', '(Optional) Icon which is used to represent the command in the UI. Either a file path or a themable configuration'),
anyOf: [
'string',
{
type: 'object',
properties: {
light: {
description: localize('vscode.extension.contributes.commandType.icon.light', 'Icon path when a light theme is used'),
type: 'string'
},
dark: {
description: localize('vscode.extension.contributes.commandType.icon.dark', 'Icon path when a dark theme is used'),
type: 'string'
}
}
}
]
}
}
};
export const commandsContribution: IJSONSchema = {
description: localize('vscode.extension.contributes.commands', "Contributes commands to the command palette."),
oneOf: [
commandType,
{
type: 'array',
items: commandType
}
]
};
}
ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: IDeclaredMenuItem[] }>('menus', schema.menusContribtion).setHandler(extensions => {
for (let extension of extensions) {
const {value, collector} = extension;
forEach(value, entry => {
if (!schema.isValidMenuItems(entry.value, collector)) {
return;
}
const menu = schema.parseMenuId(entry.key);
if (!menu) {
collector.warn(localize('menuId.invalid', "`{0}` is not a valid menu identifier", entry.key));
return;
}
MenuRegistry.registerMenuItems(menu, entry.value);
});
}
});
ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyCommand | schema.IUserFriendlyCommand[]>('commands', schema.commandsContribution).setHandler(extensions => {
const ids = new IdGenerator('contrib-cmd-icon-');
function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand , extension: IExtensionPointUser<any>) {
if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) {
return;
}
let {icon, category, title, command} = userFriendlyCommand;
let iconClass: string;
if (icon) {
iconClass = ids.nextId();
if (typeof icon === 'string') {
const path = join(extension.description.extensionFolderPath, icon);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${path}")`);
} else {
const light = join(extension.description.extensionFolderPath, icon.light);
const dark = join(extension.description.extensionFolderPath, icon.dark);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${light}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, hc-black .icon.${iconClass}`, `background-image: url("${dark}")`);
}
}
if (MenuRegistry.registerCommand({ id: command, title, category, iconClass })) {
extension.collector.info(localize('dup', "Command `{0}` appears multiple times in the `commands` section.", userFriendlyCommand.command));
}
}
for (let extension of extensions) {
const {value} = extension;
if (Array.isArray<schema.IUserFriendlyCommand>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

View file

@ -7,18 +7,54 @@
import Actions = require('vs/base/common/actions');
import WinJS = require('vs/base/common/winjs.base');
import Assert = require('vs/base/common/assert');
import Descriptors = require('vs/platform/instantiation/common/descriptors');
import Instantiation = require('vs/platform/instantiation/common/instantiation');
import {KbExpr, IKeybindings} from 'vs/platform/keybinding/common/keybindingService';
import {createDecorator, ServiceIdentifier} from 'vs/platform/instantiation/common/instantiation';
import {KbExpr, IKeybindings, IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {IDisposable} from 'vs/base/common/lifecycle';
import {createDecorator} from 'vs/platform/instantiation/common/instantiation';
export let IActionsService = createDecorator<IActionsService>('actionsService');
export interface CommandAction {
id: string;
title: string;
category?: string;
iconClass?: string;
}
export interface IActionsService {
serviceId: ServiceIdentifier<any>;
getActions(): Actions.IAction[];
export interface MenuItem {
command: CommandAction;
alt?: CommandAction;
when?: KbExpr;
}
export enum MenuId {
EditorTitle = 1,
ExplorerContext = 2
}
export const IMenuService = createDecorator<IMenuService>('menuService');
export interface IMenuService {
serviceId: any;
getMenuItems(loc: MenuId): MenuItem[];
getCommandActions(): CommandAction[];
}
export class ExecuteCommandAction extends Actions.Action {
constructor(
id: string,
label: string,
@IKeybindingService private _keybindingService: IKeybindingService) {
super(id, label);
}
run(...args: any[]): WinJS.TPromise<any> {
return this._keybindingService.executeCommand(this.id, ...args);
}
}
export class SyncActionDescriptor {

View file

@ -1,257 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {localize} from 'vs/nls';
import {Action} from 'vs/base/common/actions';
import {join} from 'vs/base/common/paths';
import {IJSONSchema} from 'vs/base/common/jsonSchema';
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {IExtensionPointUser, ExtensionsRegistry} from 'vs/platform/extensions/common/extensionsRegistry';
export interface ResourceFilter {
language?: string;
scheme?: string;
pattern?: string;
}
export type Locations = 'editor/primary' | 'editor/secondary' | 'explorer/context';
export interface ThemableIcon {
dark: string;
light: string;
}
export interface Command {
command: string;
title: string;
category?: string;
where?: Locations | Locations[];
when?: string | string[] | ResourceFilter | ResourceFilter[];
icon?: string | ThemableIcon;
}
export function isThemableIcon(thing: any): thing is ThemableIcon {
return typeof thing === 'object' && thing && typeof (<ThemableIcon>thing).dark === 'string' && typeof (<ThemableIcon>thing).light === 'string';
}
namespace validation {
function isValidWhere(where: Locations | Locations[], user: IExtensionPointUser<any>): boolean {
if (Array.isArray<Locations>(where)) {
return where.every(where => isValidWhere(where, user));
} else if (['editor/primary', 'editor/secondary', 'explorer/context'].indexOf(where) < 0) {
user.collector.error(localize('optwhere', "property `where` can be omitted or must be a valid enum value"));
return false;
}
return true;
}
function isValidWhen(when: string | string[] | ResourceFilter | ResourceFilter[], user: IExtensionPointUser<any>): boolean {
if (Array.isArray<string | ResourceFilter>(when)) {
for (let w of when) {
if (!isValidWhen(w, user)) {
return false;
}
}
} else if (typeof when === 'string' || typeof when === 'object') {
return true;
}
user.collector.error(localize('requirefilter', "property `when` is mandatory and must be a string or like `{language, scheme, pattern}`"));
return false;
}
function isValidIcon(icon: string | ThemableIcon, user: IExtensionPointUser<any>): boolean {
if (typeof icon === 'undefined') {
return true;
}
if (typeof icon === 'string') {
return true;
}
if (typeof icon === 'object' && typeof (<ThemableIcon>icon).dark === 'string' && typeof (<ThemableIcon>icon).light === 'string') {
return true;
}
user.collector.error(localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
return false;
}
export function isValidCommand(candidate: Command, user: IExtensionPointUser<any>): boolean {
if (!candidate) {
user.collector.error(localize('nonempty', "expected non-empty value."));
return false;
}
if (typeof candidate.command !== 'string') {
user.collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
return false;
}
if (typeof candidate.title !== 'string') {
user.collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title'));
return false;
}
if (candidate.category && typeof candidate.category !== 'string') {
user.collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'category'));
return false;
}
if (!isValidIcon(candidate.icon, user)) {
return false;
}
// make icon paths absolute
let {icon} = candidate;
if (typeof icon === 'string') {
candidate.icon = join(user.description.extensionFolderPath, icon);
} else if(isThemableIcon(icon)) {
icon.dark = join(user.description.extensionFolderPath, icon.dark);
icon.light = join(user.description.extensionFolderPath, icon.light);
}
return true;
}
}
namespace schema {
const filterType: IJSONSchema = {
type: 'object',
properties: {
language: {
description: localize('vscode.extension.contributes.filterType.language', ""),
type: 'string'
},
scheme: {
description: localize('vscode.extension.contributes.filterType.scheme', ""),
type: 'string'
},
pattern: {
description: localize('vscode.extension.contributes.filterType.pattern', ""),
type: 'string'
}
}
};
const contextType: IJSONSchema = {
type: 'object',
properties: {
where: {
description: localize('vscode.extension.contributes.commandType.context.where', "Menus and tool bars to which commands can be added, e.g. `editor title actions` or `explorer context menu`"),
enum: [
'editor/primary',
'editor/secondary'
]
},
when: {
description: localize('vscode.extension.contributes.commandType.context.when', "Condition that must be met in order to show the command. Can be a language identifier, a glob-pattern, an uri scheme, or a combination of them."),
anyOf: [
'string',
filterType,
{ type: 'array', items: 'string' },
{ type: 'array', items: filterType },
]
},
icon: {
description: localize('vscode.extension.contributes.commandType.icon', '(Optional) Icon which is used to represent the command in the UI. Either a file path or a themable configuration'),
oneOf: [
'string',
{
type: 'object',
properties: {
light: {
description: localize('vscode.extension.contributes.commandType.icon.light', 'Icon path when a light theme is used'),
type: 'string'
},
dark: {
description: localize('vscode.extension.contributes.commandType.icon.dark', 'Icon path when a dark theme is used'),
type: 'string'
}
}
}
]
}
}
};
const commandType: IJSONSchema = {
type: 'object',
properties: {
command: {
description: localize('vscode.extension.contributes.commandType.command', 'Identifier of the command to execute'),
type: 'string'
},
title: {
description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'),
type: 'string'
},
category: {
description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'),
type: 'string'
},
context: {
description: localize('vscode.extension.contributes.commandType.context', '(Optional) Define places where the command should show in addition to the Command palette'),
oneOf: [
contextType,
{ type: 'array', items: contextType }
]
}
}
};
export const commandContribution: IJSONSchema = {
description: localize('vscode.extension.contributes.commands', "Contributes commands to the command palette."),
oneOf: [
commandType,
{
type: 'array',
items: commandType
}
]
};
}
export const commands: Command[] = [];
function handleCommand(command: Command, user: IExtensionPointUser<any>): void {
if (validation.isValidCommand(command, user)) {
// store command globally
commands.push(command);
}
}
ExtensionsRegistry.registerExtensionPoint<Command | Command[]>('commands', schema.commandContribution).setHandler(extensions => {
for (let extension of extensions) {
const {value} = extension;
if (Array.isArray<Command>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
Object.freeze(commands);
});
export class CommandAction extends Action {
constructor(
public command: Command,
@IExtensionService extensionService: IExtensionService,
@IKeybindingService keybindingService: IKeybindingService
) {
super(command.command, command.title);
this.order = Number.MAX_VALUE;
const activationEvent = `onCommand:${command.command}`;
this._actionCallback = (...args: any[]) => {
return extensionService.activateByEvent(activationEvent).then(() => {
return keybindingService.executeCommand(command.command, ...args);
});
};
}
}

View file

@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import {IKeybindingService, IKeybindingContextKey} from 'vs/platform/keybinding/common/keybindingService';
import {IModeService} from 'vs/editor/common/services/modeService';
export class ResourceContextKey implements IKeybindingContextKey<URI> {
static Scheme = 'resourceScheme';
static LangId = 'resourceLangId';
static Resource = 'resource';
private _resourceKey: IKeybindingContextKey<URI>;
private _schemeKey: IKeybindingContextKey<string>;
private _langIdKey: IKeybindingContextKey<string>;
constructor(
@IKeybindingService keybindingService: IKeybindingService,
@IModeService private _modeService: IModeService
) {
this._schemeKey = keybindingService.createKey(ResourceContextKey.Scheme, undefined);
this._langIdKey = keybindingService.createKey(ResourceContextKey.LangId, undefined);
this._resourceKey = keybindingService.createKey(ResourceContextKey.Resource, undefined);
}
set(value: URI) {
this._resourceKey.set(value);
this._schemeKey.set(value && value.scheme);
this._langIdKey.set(value && this._modeService.getModeIdByFilenameOrFirstLine(value.fsPath));
}
reset(): void {
this._schemeKey.reset();
this._langIdKey.reset();
this._resourceKey.reset();
}
}

View file

@ -1,215 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {Registry} from 'vs/platform/platform';
import URI from 'vs/base/common/uri';
import {IAction, Action} from 'vs/base/common/actions';
import {IDisposable} from 'vs/base/common/lifecycle';
import {BaseActionItem, ActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {Scope, IActionBarRegistry, Extensions, ActionBarContributor} from 'vs/workbench/browser/actionBarRegistry';
import {IModeService} from 'vs/editor/common/services/modeService';
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
import {IThemeService} from 'vs/workbench/services/themes/common/themeService';
import {isLightTheme} from 'vs/platform/theme/common/themes';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {commands, CommandAction, Command, Locations} from '../common/commandsExtensionPoint';
import matches from 'vs/editor/common/modes/languageSelector';
import {EditorInput} from 'vs/workbench/common/editor';
class ResolvedCommand {
constructor(
private _command: Command,
@IInstantiationService private _instantiationService: IInstantiationService,
@IThemeService private _themeService: IThemeService,
@IModeService private _modeService: IModeService
) {
}
matches(location: Locations, resource: URI): boolean {
const {where, when} = this._command;
if (!where || !when) {
return false;
}
// (1) check for location
if (Array.isArray<Locations>(where)) {
if (where.every(where => where !== location)) {
return false;
}
} else if (where !== location) {
return false;
}
// (2) check for resource
if (!matches(when, resource, this._modeService.getModeIdByFilenameOrFirstLine(resource.fsPath))) {
return false;
}
return true;
}
createAction(resource: URI): ScopedCommandAction {
return this._instantiationService.createInstance(ScopedCommandAction, this._command, resource);
}
}
class ScopedCommandAction extends CommandAction {
private _themeListener: IDisposable;
constructor(
command: Command,
private _resource: URI,
@IThemeService private _themeService: IThemeService,
@IExtensionService extensionService: IExtensionService,
@IKeybindingService keybindingService: IKeybindingService
) {
super(command, extensionService, keybindingService);
}
dispose() {
this._themeListener.dispose();
super.dispose();
}
get icon(): string {
const {icon} = this.command;
if (!icon) {
return;
}
if (typeof icon === 'string') {
return icon;
} else {
return isLightTheme(this._themeService.getTheme())
? icon.light
: icon.dark;
}
}
run() {
return super.run(this._resource);
}
}
abstract class BaseActionBarContributor extends ActionBarContributor {
private _isReady: boolean = false;
private _contributedActions: ResolvedCommand[];
constructor(
@IExtensionService private _extensionService: IExtensionService,
@IInstantiationService private _instantationService: IInstantiationService
) {
super();
this._extensionService.onReady().then(() => {
this._contributedActions = commands.map(command => _instantationService.createInstance(ResolvedCommand, command));
this._isReady = true;
});
}
protected abstract _wheres(): { primary: Locations; secondary: Locations };
protected abstract _getResource(context: any): URI;
public hasActions(context: any): boolean {
return this._isReady && this._wheres().primary && this.getActions(context).length > 0;
}
public hasSecondaryActions(context: any): boolean {
return this._isReady && this._wheres().secondary && this.getSecondaryActions(context).length > 0;
}
public getActions(context: any): IAction[] {
return this._getActions(context, this._wheres().primary);
}
public getSecondaryActions(context: any): IAction[] {
return this._getActions(context, this._wheres().secondary);
}
private _getActions(context: any, where: Locations): IAction[] {
const uri = this._getResource(context);
const result: IAction[] = [];
if (uri) {
for (let command of this._contributedActions) {
if (command.matches(where, uri)) {
result.push(command.createAction(uri));
}
}
}
return result;
}
public getActionItem(context: any, action: Action): BaseActionItem {
if (action instanceof ScopedCommandAction) {
return this._instantationService.createInstance(CommandActionItem, action);
}
}
}
class EditorContributor extends BaseActionBarContributor {
protected _wheres(): { primary: Locations; secondary: Locations } {
return { primary: 'editor/primary', secondary: 'editor/secondary' };
}
protected _getResource(context: any): URI {
const {input} = context;
if (input instanceof EditorInput) {
if (typeof input.getResource === 'function') {
const candidate = input.getResource();
if (candidate instanceof URI) {
return candidate;
}
}
}
}
}
class ContextMenuContributor extends BaseActionBarContributor {
protected _wheres(): { primary: Locations; secondary: Locations } {
return { secondary: 'explorer/context', primary: undefined };
}
protected _getResource(context: any): URI {
if (context.element) {
if (context.element.resource instanceof URI) {
return <URI> context.element.resource;
}
}
}
}
class CommandActionItem extends ActionItem {
constructor(
action: ScopedCommandAction,
@IThemeService private _themeService: IThemeService
) {
super(undefined, action, { icon: Boolean(action.icon), label: !Boolean(action.icon) });
this._themeService.onDidThemeChange(this._updateClass, this, this.callOnDispose);
}
_updateClass(): void {
super._updateClass();
const element = this.$e.getHTMLElement();
const {icon} = <ScopedCommandAction>this._action;
if (icon && element.classList.contains('icon')) {
element.style.backgroundImage = `url("${icon}")`;
}
}
onClick(event: Event): void {
super.onClick(event);
}
}
Registry.as<IActionBarRegistry>(Extensions.Actionbar).registerActionBarContributor(Scope.EDITOR, EditorContributor);
Registry.as<IActionBarRegistry>(Extensions.Actionbar).registerActionBarContributor(Scope.VIEWER, ContextMenuContributor);

View file

@ -1,46 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {localize} from 'vs/nls';
import {IAction} from 'vs/base/common/actions';
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {IActionsService} from '../common/actions';
import {commands, CommandAction} from '../common/commandsExtensionPoint';
import 'vs/platform/actions/workbench/actionBarContributions';
export default class ActionsService implements IActionsService {
serviceId: any;
private _extensionsActions: IAction[];
constructor(
@IExtensionService private _extensionService: IExtensionService,
@IKeybindingService private _keybindingsService: IKeybindingService
) {
this._extensionService.onReady().then(() => this._extensionsActions = null);
}
getActions(): IAction[] {
if (!this._extensionsActions) {
this._extensionsActions = [];
for (let command of commands) {
const action = new CommandAction(command, this._extensionService, this._keybindingsService);
action.order = Number.MAX_VALUE;
action.label = command.category
? localize('category.label', "{0}: {1}", command.category, command.title)
: command.title;
this._extensionsActions.push(action);
}
}
return this._extensionsActions.slice(0);
}
}

View file

@ -16,12 +16,13 @@ import * as dom from 'vs/base/browser/dom';
import {IKeyboardEvent, StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {KeybindingResolver} from 'vs/platform/keybinding/common/keybindingResolver';
import {ICommandHandler, ICommandHandlerDescription, IKeybindingContextKey, IKeybindingItem, IKeybindingScopeLocation, IKeybindingService, SET_CONTEXT_COMMAND_ID} from 'vs/platform/keybinding/common/keybindingService';
import {ICommandHandler, ICommandHandlerDescription, IKeybindingContextKey, IKeybindingItem, IKeybindingScopeLocation, IKeybindingService, SET_CONTEXT_COMMAND_ID, KbExpr} from 'vs/platform/keybinding/common/keybindingService';
import {KeybindingsRegistry} from 'vs/platform/keybinding/common/keybindingsRegistry';
import {IStatusbarService} from 'vs/platform/statusbar/common/statusbar';
import {IMessageService} from 'vs/platform/message/common/message';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {ServicesAccessor} from 'vs/platform/instantiation/common/instantiation';
import Event, {Emitter, debounceEvent} from 'vs/base/common/event';
let KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
@ -37,14 +38,17 @@ export class KeybindingContext {
this._value['_contextId'] = id;
}
public setValue(key: string, value: any): void {
// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);
this._value[key] = value;
public setValue(key: string, value: any): boolean {
// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);
if (this._value[key] !== value) {
this._value[key] = value;
return true;
}
}
public removeValue(key: string): void {
// console.log('REMOVE ' + key + ' FROM ' + this._id);
delete this._value[key];
public removeValue(key: string): boolean {
// console.log('REMOVE ' + key + ' FROM ' + this._id);
return delete this._value[key];
}
public fillInContext(bucket: any): void {
@ -131,11 +135,15 @@ class KeybindingContextKey<T> implements IKeybindingContextKey<T> {
export abstract class AbstractKeybindingService {
public serviceId = IKeybindingService;
protected _onDidChangeContext: Event<string[]>;
protected _onDidChangeContextKey: Emitter<string>;
protected _myContextId: number;
protected _instantiationService: IInstantiationService;
constructor(myContextId: number) {
this._myContextId = myContextId;
this._onDidChangeContextKey = new Emitter<string>();
this._instantiationService = null;
}
@ -143,20 +151,42 @@ export abstract class AbstractKeybindingService {
return new KeybindingContextKey(this, key, defaultValue);
}
public abstract contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean;
public abstract getContextValue<T>(domNode: HTMLElement, key: string): T;
public get onDidChangeContext(): Event<string[]> {
if (!this._onDidChangeContext) {
this._onDidChangeContext = debounceEvent(this._onDidChangeContextKey.event, (prev: string[], cur) => {
if (!prev) {
prev = [cur];
} else if (prev.indexOf(cur) < 0) {
prev.push(cur);
}
return prev;
}, 25);
}
return this._onDidChangeContext;
}
public setInstantiationService(instantiationService: IInstantiationService): void {
this._instantiationService = instantiationService;
}
public createScoped(domNode: IKeybindingScopeLocation): IKeybindingService {
return new ScopedKeybindingService(this, domNode);
return new ScopedKeybindingService(this, this._onDidChangeContextKey, domNode);
}
public setContext(key: string, value: any): void {
this.getContext(this._myContextId).setValue(key, value);
if(this.getContext(this._myContextId).setValue(key, value)) {
this._onDidChangeContextKey.fire(key);
}
}
public removeContext(key: string): void {
this.getContext(this._myContextId).removeValue(key);
if(this.getContext(this._myContextId).removeValue(key)) {
this._onDidChangeContextKey.fire(key);
}
}
public hasCommand(commandId: string): boolean {
@ -227,6 +257,21 @@ export abstract class KeybindingService extends AbstractKeybindingService implem
this._toDispose = dispose(this._toDispose);
}
public contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean {
const ctx = Object.create(null);
this.getContext(this._findContextAttr(domNode)).fillInContext(ctx);
this._configurationContext.fillInContext(ctx);
// console.log(JSON.stringify(contextValue, null, '\t'));
return KeybindingResolver.contextMatchesRules(ctx, rules);
}
public getContextValue<T>(domNode: HTMLElement, key: string): T {
const ctx = Object.create(null);
this.getContext(this._findContextAttr(domNode)).fillInContext(ctx);
this._configurationContext.fillInContext(ctx);
return <T>ctx[key];
}
public getLabelFor(keybinding: Keybinding): string {
return keybinding._toUSLabel();
}
@ -397,9 +442,10 @@ class ScopedKeybindingService extends AbstractKeybindingService {
private _parent: AbstractKeybindingService;
private _domNode: IKeybindingScopeLocation;
constructor(parent: AbstractKeybindingService, domNode: IKeybindingScopeLocation) {
constructor(parent: AbstractKeybindingService, emitter: Emitter<string>, domNode: IKeybindingScopeLocation) {
super(parent.createChildContext());
this._parent = parent;
this._onDidChangeContextKey = emitter;
this._domNode = domNode;
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
@ -409,6 +455,18 @@ class ScopedKeybindingService extends AbstractKeybindingService {
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
}
public get onDidChangeContext(): Event<string[]> {
return this._parent.onDidChangeContext;
}
public contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean {
return this._parent.contextMatchesRules(domNode, rules);
}
public getContextValue<T>(domNode: HTMLElement, key: string): T {
return this._parent.getContextValue<T>(domNode, key);
}
public getLabelFor(keybinding: Keybinding): string {
return this._parent.getLabelFor(keybinding);
}

View file

@ -9,6 +9,7 @@ import {Keybinding} from 'vs/base/common/keyCodes';
import {TypeConstraint} from 'vs/base/common/types';
import {TPromise} from 'vs/base/common/winjs.base';
import {ServiceIdentifier, ServicesAccessor, createDecorator} from 'vs/platform/instantiation/common/instantiation';
import Event from 'vs/base/common/event';
export interface IUserFriendlyKeybinding {
key: string;
@ -435,13 +436,18 @@ export let IKeybindingService = createDecorator<IKeybindingService>('keybindingS
export interface IKeybindingScopeLocation {
setAttribute(attr: string, value: string): void;
removeAttribute(attr: string): void;
hasAttribute(attr: string): boolean;
getAttribute(attr: string): string;
}
export interface IKeybindingService {
serviceId: ServiceIdentifier<any>;
dispose(): void;
onDidChangeContext: Event<string[]>;
createKey<T>(key: string, defaultValue: T): IKeybindingContextKey<T>;
contextMatchesRules(domNode: IKeybindingScopeLocation, rules: KbExpr): boolean;
getContextValue<T>(domNode: IKeybindingScopeLocation, key: string): T;
createScoped(domNode: IKeybindingScopeLocation): IKeybindingService;

View file

@ -7,7 +7,8 @@
import {IHTMLContentElement} from 'vs/base/common/htmlContent';
import {Keybinding} from 'vs/base/common/keyCodes';
import {TPromise} from 'vs/base/common/winjs.base';
import {IKeybindingContextKey, IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import Event from 'vs/base/common/event';
import {IKeybindingContextKey, IKeybindingService, KbExpr} from 'vs/platform/keybinding/common/keybindingService';
class MockKeybindingContextKey<T> implements IKeybindingContextKey<T> {
private _key: string;
@ -39,6 +40,15 @@ export class MockKeybindingService implements IKeybindingService {
public createKey<T>(key: string, defaultValue: T): IKeybindingContextKey<T> {
return new MockKeybindingContextKey(key, defaultValue);
}
public contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean {
return false;
}
public get onDidChangeContext(): Event<string[]> {
return Event.None;
}
public getContextValue(domNode: HTMLElement, key: string) {
return;
}
public getLabelFor(keybinding: Keybinding): string {
return keybinding._toUSLabel();

View file

@ -372,7 +372,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Otherwise instantiate
let progressService = new WorkbenchProgressService(this.eventService, this.sideBySideControl.getProgressBar(position), descriptor.getId(), true);
let editorInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));
let editorInstantiationService = this.sideBySideControl.getInstantiationService(position).createChild(new ServiceCollection([IProgressService, progressService]));
let loaded = false;
const onInstantiate = (arg: BaseEditor | Error): TPromise<BaseEditor | Error> => {

View file

@ -56,6 +56,7 @@ export class NoTabsTitleControl extends TitleControl {
}
public create(parent: HTMLElement): void {
super.create(parent);
this.titleContainer = parent;
// Pin on double click

View file

@ -26,6 +26,7 @@ import {IMessageService} from 'vs/platform/message/common/message';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
@ -65,6 +66,7 @@ export interface ISideBySideEditorControl {
isDragging(): boolean;
getInstantiationService(position: Position): IInstantiationService;
getProgressBar(position: Position): ProgressBar;
updateProgress(position: Position, state: ProgressState): void;
@ -92,6 +94,8 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
private dimension: Dimension;
private dragging: boolean;
private instantiationServices: IInstantiationService[];
private containers: Builder[];
private containerWidth: number[];
private containerInitialRatios: number[];
@ -136,6 +140,8 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
this.parent = parent;
this.dimension = new Dimension(0, 0);
this.instantiationServices = [];
this.containers = [];
this.containerWidth = [];
@ -159,7 +165,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
private registerListeners(): void {
this.toDispose.push(this.stacks.onModelChanged(e => this.onStacksChanged(e)));
this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config)));
this.extensionService.onReady().then(() => POSITIONS.forEach(position => this.titleAreaControl[position].refresh()));
this.extensionService.onReady().then(() => this.onExtensionsReady());
}
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
@ -181,6 +187,12 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
});
}
private onExtensionsReady(): void {
// Up to date title areas
POSITIONS.forEach(position => this.titleAreaControl[position].refresh());
}
private onStacksChanged(e: IStacksModelChangeEvent): void {
// Up to date context
@ -738,6 +750,14 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
// Right Container
this.containers[Position.RIGHT] = $(parent).div({ class: 'one-editor-container editor-right monaco-editor-background' });
// InstantiationServices
POSITIONS.forEach(position => {
this.instantiationServices[position] = this.instantiationService.createChild(new ServiceCollection(
[IKeybindingService, this.keybindingService.createScoped(this.containers[position].getHTMLElement())]
// [IProgressService, ]
));
});
// Title containers
POSITIONS.forEach(position => {
this.titleContainer[position] = $(this.containers[position]).div({ 'class': 'title' });
@ -982,7 +1002,7 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
private createTitleControl(position: Position): void {
const useTabs = !!this.configurationService.getConfiguration<IWorkbenchEditorConfiguration>().workbench.editor.showTabs;
this.titleAreaControl[position] = useTabs ? this.instantiationService.createInstance(TabsTitleControl) : this.instantiationService.createInstance(NoTabsTitleControl);
this.titleAreaControl[position] = this.instantiationServices[position].createInstance<ITitleAreaControl>(useTabs ? TabsTitleControl : NoTabsTitleControl);
this.titleAreaControl[position].create(this.titleContainer[position].getHTMLElement());
this.titleAreaControl[position].setContext(this.stacks.groupAt(position));
this.titleAreaControl[position].refresh();
@ -1565,6 +1585,10 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti
}
}
public getInstantiationService(position: Position): IInstantiationService {
return this.instantiationServices[position];
}
public getProgressBar(position: Position): ProgressBar {
return this.progressBar[position];
}

View file

@ -73,6 +73,7 @@ export class TabsTitleControl extends TitleControl {
}
public create(parent: HTMLElement): void {
super.create(parent);
this.titleContainer = parent;
// Tabs Container

View file

@ -15,7 +15,7 @@ import DOM = require('vs/base/browser/dom');
import {TPromise} from 'vs/base/common/winjs.base';
import {BaseEditor, IEditorInputActionContext} from 'vs/workbench/browser/parts/editor/baseEditor';
import {RunOnceScheduler} from 'vs/base/common/async';
import {IEditorStacksModel, IEditorGroup, IEditorIdentifier, EditorInput, IWorkbenchEditorConfiguration, IStacksModelChangeEvent} from 'vs/workbench/common/editor';
import {IEditorStacksModel, IEditorGroup, IEditorIdentifier, EditorInput, IWorkbenchEditorConfiguration, IStacksModelChangeEvent, getResource} from 'vs/workbench/common/editor';
import {EventType as BaseEventType} from 'vs/base/common/events';
import {IActionItem, ActionsOrientation, Separator} from 'vs/base/browser/ui/actionbar/actionbar';
import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar';
@ -31,6 +31,9 @@ import {IInstantiationService} from 'vs/platform/instantiation/common/instantiat
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {CloseEditorsInGroupAction, MoveGroupLeftAction, MoveGroupRightAction, SplitEditorAction, CloseEditorAction, KeepEditorAction, CloseOtherEditorsInGroupAction, CloseRightEditorsInGroupAction, ShowEditorsInGroupAction} from 'vs/workbench/browser/parts/editor/editorActions';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import {ActionBarContributor} from 'vs/platform/actions/browser/actionBarContributor';
import {MenuId} from 'vs/platform/actions/common/actions';
import {ResourceContextKey} from 'vs/platform/actions/common/resourceContextKey';
export interface IToolbarActions {
primary: IAction[];
@ -47,7 +50,7 @@ export interface ITitleAreaControl {
dispose(): void;
}
export abstract class TitleControl {
export abstract class TitleControl implements ITitleAreaControl {
private static draggedEditor: IEditorIdentifier;
@ -72,6 +75,9 @@ export abstract class TitleControl {
private scheduler: RunOnceScheduler;
private refreshScheduled: boolean;
private resourceContext: ResourceContextKey;
private titleActionBarContributor: ActionBarContributor;
constructor(
@IContextMenuService protected contextMenuService: IContextMenuService,
@IInstantiationService protected instantiationService: IInstantiationService,
@ -91,6 +97,8 @@ export abstract class TitleControl {
this.scheduler = new RunOnceScheduler(() => this.onSchedule(), 0);
this.toDispose.push(this.scheduler);
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
this.initActions();
this.registerListeners();
}
@ -168,6 +176,12 @@ export abstract class TitleControl {
}
}
public create(parent: HTMLElement): void {
this.titleActionBarContributor = this.instantiationService.createInstance(ActionBarContributor, parent, MenuId.EditorTitle);
this.toDispose.push(this.titleActionBarContributor.onDidUpdate(e => this.refresh()));
this.toDispose.push(this.titleActionBarContributor);
}
protected abstract doRefresh(): void;
protected doUpdate(): void {
@ -250,6 +264,11 @@ export abstract class TitleControl {
actionItem = actionBarRegistry.getActionItemForContext(Scope.EDITOR, { input: editor && editor.input, editor, position }, action);
}
// Check extensions
if (!actionItem) {
actionItem = this.titleActionBarContributor.getActionItem(action);
}
return actionItem;
}
@ -260,6 +279,9 @@ export abstract class TitleControl {
const {group} = identifier;
const position = this.stacks.positionOfGroup(group);
// Update the resource context
this.resourceContext.set(group && getResource(group.activeEditor));
// Editor actions require the editor control to be there, so we retrieve it via service
const control = this.editorService.getVisibleEditors()[position];
if (this.stacks.isActive(group) && control instanceof BaseEditor && control.input && typeof control.position === 'number') {
@ -277,14 +299,15 @@ export abstract class TitleControl {
const editorInputActions = this.getEditorActionsForContext({ input: control.input, editor: control, position: control.position });
primary.push(...editorInputActions.primary);
secondary.push(...editorInputActions.secondary);
// MenuItems
primary.push(...this.titleActionBarContributor.getActions());
}
return { primary, secondary };
}
private getEditorActionsForContext(context: BaseEditor): IToolbarActions;
private getEditorActionsForContext(context: IEditorInputActionContext): IToolbarActions;
private getEditorActionsForContext(context: any): IToolbarActions {
private getEditorActionsForContext(context: BaseEditor | IEditorInputActionContext): IToolbarActions {
const primaryActions: IAction[] = [];
const secondaryActions: IAction[] = [];

View file

@ -57,14 +57,15 @@ import {IEditorGroupService} from 'vs/workbench/services/group/common/groupServi
import {IHistoryService} from 'vs/workbench/services/history/common/history';
import {IEventService} from 'vs/platform/event/common/event';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {SyncDescriptor} from 'vs/platform/instantiation/common/descriptors';
import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection';
import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle';
import {IMessageService} from 'vs/platform/message/common/message';
import {IThreadService} from 'vs/platform/thread/common/thread';
import {MainThreadService} from 'vs/platform/thread/common/mainThreadService';
import {IStatusbarService} from 'vs/platform/statusbar/common/statusbar';
import {IActionsService} from 'vs/platform/actions/common/actions';
import ActionsService from 'vs/platform/actions/workbench/actionsService';
import {IMenuService} from 'vs/platform/actions/common/actions';
import {MenuService} from 'vs/platform/actions/browser/menuService';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
interface WorkbenchParams {
@ -346,8 +347,8 @@ export class Workbench implements IPartService {
// Context Menu
serviceCollection.set(IContextMenuService, this.instantiationService.createInstance(ContextMenuService));
// Actions
serviceCollection.set(IActionsService, this.instantiationService.createInstance(ActionsService));
// Menus/Actions
serviceCollection.set(IMenuService, new SyncDescriptor(MenuService));
// Viewlet service (sidebar part)
this.sidebarPart = this.instantiationService.createInstance(SidebarPart, Identifiers.SIDEBAR_PART);

View file

@ -573,7 +573,17 @@ export function getUntitledOrFileResource(input: IEditorInput, supportDiff?: boo
// File
let fileInput = asFileEditorInput(input, supportDiff);
return fileInput && fileInput.getResource();
return fileInput && fileInput && fileInput.getResource();
}
export function getResource(input: IEditorInput): URI {
if (input && typeof (<any> input).getResource === 'function') {
let candidate = (<any>input).getResource();
if (candidate instanceof URI) {
return candidate;
}
}
return getUntitledOrFileResource(input, true);
}
/**

View file

@ -41,6 +41,7 @@ import {IProgressService} from 'vs/platform/progress/common/progress';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {ResourceContextKey} from 'vs/platform/actions/common/resourceContextKey';
export class ExplorerView extends CollapsibleViewletView {
@ -60,6 +61,8 @@ export class ExplorerView extends CollapsibleViewletView {
private explorerRefreshDelayer: ThrottledDelayer<void>;
private explorerImportDelayer: ThrottledDelayer<void>;
private resourceContext: ResourceContextKey;
private shouldRefresh: boolean;
private autoReveal: boolean;
@ -96,6 +99,8 @@ export class ExplorerView extends CollapsibleViewletView {
this.explorerRefreshDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY);
this.explorerImportDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_IMPORT_REFRESH_DELAY);
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
}
public renderHeader(container: HTMLElement): void {
@ -349,6 +354,9 @@ export class ExplorerView extends CollapsibleViewletView {
this.toDispose.push(this.eventService.addListener2('files.internal:fileChanged', (e: LocalFileChangeEvent) => this.onLocalFileChange(e)));
this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_CHANGES, (e: FileChangesEvent) => this.onFileChanges(e)));
// Update resource context based on focused element
this.toDispose.push(this.explorerViewer.addListener2('focus', (e: { focus: FileStat }) => this.resourceContext.set(e.focus && e.focus.resource)));
return this.explorerViewer;
}

View file

@ -46,6 +46,8 @@ import {IProgressService} from 'vs/platform/progress/common/progress';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {Keybinding, CommonKeybindings} from 'vs/base/common/keyCodes';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {ActionBarContributor} from 'vs/platform/actions/browser/actionBarContributor';
import {MenuId} from 'vs/platform/actions/common/actions';
export class FileDataSource implements IDataSource {
private workspace: IWorkspace;
@ -354,6 +356,8 @@ export class FileController extends DefaultController {
private didCatchEnterDown: boolean;
private state: FileViewletState;
private contextMenuActions: ActionBarContributor;
private workspace: IWorkspace;
constructor(state: FileViewletState,
@ -462,6 +466,11 @@ export class FileController extends DefaultController {
return false;
}
if (!this.contextMenuActions) {
this.contextMenuActions = this.instantiationService.createInstance(ActionBarContributor,
tree.getHTMLElement(), MenuId.ExplorerContext);
}
event.preventDefault();
event.stopPropagation();
@ -474,7 +483,12 @@ export class FileController extends DefaultController {
let anchor = { x: event.posx + 1, y: event.posy };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.state.actionProvider.getSecondaryActions(tree, stat),
getActions: () => {
return this.state.actionProvider.getSecondaryActions(tree, stat).then(actions => {
// TODO@joh sorting,grouping
return [...this.contextMenuActions.getActions(), ...actions];
});
},
getActionItem: this.state.actionProvider.getActionItem.bind(this.state.actionProvider, tree, stat),
getKeyBinding: (a): Keybinding => keybindingForAction(a.id),
getActionsContext: () => {

View file

@ -16,7 +16,7 @@ import {IAction, Action} from 'vs/base/common/actions';
import {toErrorMessage} from 'vs/base/common/errors';
import {Mode, IEntryRunContext, IAutoFocus} from 'vs/base/parts/quickopen/common/quickOpen';
import {QuickOpenEntryGroup, IHighlight, QuickOpenModel} from 'vs/base/parts/quickopen/browser/quickOpenModel';
import {SyncActionDescriptor, IActionsService} from 'vs/platform/actions/common/actions';
import {SyncActionDescriptor, ExecuteCommandAction, IMenuService} from 'vs/platform/actions/common/actions';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry';
import {Registry} from 'vs/platform/platform';
import {QuickOpenHandler, QuickOpenAction} from 'vs/workbench/browser/quickopen';
@ -229,7 +229,7 @@ export class CommandsHandler extends QuickOpenHandler {
@IInstantiationService private instantiationService: IInstantiationService,
@IMessageService private messageService: IMessageService,
@IKeybindingService private keybindingService: IKeybindingService,
@IActionsService private actionsService: IActionsService
@IMenuService private menuService: IMenuService
) {
super();
}
@ -260,7 +260,7 @@ export class CommandsHandler extends QuickOpenHandler {
let editorEntries = this.editorActionsToEntries(editorActions, searchValue);
// Other Actions
let otherActions = this.actionsService.getActions();
let otherActions = this.menuService.getCommandActions().map(command => new ExecuteCommandAction(command.id, command.category ? nls.localize('', "{0}: {1}", command.category, command.title) : command.title, this.keybindingService));
let otherEntries = this.otherActionsToEntries(otherActions, searchValue);
// Concat

View file

@ -16,6 +16,9 @@ import 'vs/editor/browser/editor.all';
// Languages
import 'vs/languages/languages.main';
// Menus/Actions
import 'vs/platform/actions/browser/menusExtensionPoint';
// Workbench
import 'vs/workbench/browser/actions/toggleStatusbarVisibility';
import 'vs/workbench/browser/actions/toggleSidebarVisibility';