diff --git a/extensions/markdown/package.json b/extensions/markdown/package.json index e4f4944e422..5f800d1199e 100644 --- a/extensions/markdown/package.json +++ b/extensions/markdown/package.json @@ -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." } } diff --git a/extensions/markdown/package.nls.json b/extensions/markdown/package.nls.json index f5269c03855..dbf773dc25c 100644 --- a/extensions/markdown/package.nls.json +++ b/extensions/markdown/package.nls.json @@ -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" } \ No newline at end of file diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 46207f71a75..2f5af7cd36b 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -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); diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index f3ad51352d5..cd8aff710a1 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -159,6 +159,22 @@ export function filterEvent(event: Event, filter: (e:T)=>boolean): Event event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } +export function debounceEvent(event: Event, merger: (last: O, event: I) => O, delay: number = 100): Event { + 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 diff --git a/src/vs/editor/test/common/mocks/mockCodeEditor.ts b/src/vs/editor/test/common/mocks/mockCodeEditor.ts index 91faac1ded8..b7ca13a3556 100644 --- a/src/vs/editor/test/common/mocks/mockCodeEditor.ts +++ b/src/vs/editor/test/common/mocks/mockCodeEditor.ts @@ -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 { diff --git a/src/vs/platform/actions/browser/actionBarContributor.ts b/src/vs/platform/actions/browser/actionBarContributor.ts new file mode 100644 index 00000000000..55f5adfb7cf --- /dev/null +++ b/src/vs/platform/actions/browser/actionBarContributor.ts @@ -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(); + 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 { + 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(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} = this._action; + return this._altKeyDown && altCommand || command; + } + + onClick(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + + (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); + } + } +} diff --git a/src/vs/platform/actions/browser/menuService.ts b/src/vs/platform/actions/browser/menuService.ts new file mode 100644 index 00000000000..eef2be16b12 --- /dev/null +++ b/src/vs/platform/actions/browser/menuService.ts @@ -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); + } +} diff --git a/src/vs/platform/actions/browser/menusExtensionPoint.ts b/src/vs/platform/actions/browser/menusExtensionPoint.ts new file mode 100644 index 00000000000..fc594e77757 --- /dev/null +++ b/src/vs/platform/actions/browser/menusExtensionPoint.ts @@ -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('commands', schema.commandsContribution).setHandler(extensions => { + + const ids = new IdGenerator('contrib-cmd-icon-'); + + function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand , extension: IExtensionPointUser) { + + 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(value)) { + for (let command of value) { + handleCommand(command, extension); + } + } else { + handleCommand(value, extension); + } + } +}); + diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index cd1d1bb4f51..e7c1e54b4d1 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -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('actionsService'); +export interface CommandAction { + id: string; + title: string; + category?: string; + iconClass?: string; +} -export interface IActionsService { - serviceId: ServiceIdentifier; - getActions(): Actions.IAction[]; +export interface MenuItem { + command: CommandAction; + alt?: CommandAction; + when?: KbExpr; +} + +export enum MenuId { + EditorTitle = 1, + ExplorerContext = 2 +} + +export const IMenuService = createDecorator('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 { + return this._keybindingService.executeCommand(this.id, ...args); + } } export class SyncActionDescriptor { diff --git a/src/vs/platform/actions/common/commandsExtensionPoint.ts b/src/vs/platform/actions/common/commandsExtensionPoint.ts deleted file mode 100644 index b10db1143d0..00000000000 --- a/src/vs/platform/actions/common/commandsExtensionPoint.ts +++ /dev/null @@ -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 (thing).dark === 'string' && typeof (thing).light === 'string'; -} - - -namespace validation { - - function isValidWhere(where: Locations | Locations[], user: IExtensionPointUser): boolean { - if (Array.isArray(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): boolean { - if (Array.isArray(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): boolean { - if (typeof icon === 'undefined') { - return true; - } - if (typeof icon === 'string') { - return true; - } - if (typeof icon === 'object' && typeof (icon).dark === 'string' && typeof (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): 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): void { - if (validation.isValidCommand(command, user)) { - // store command globally - commands.push(command); - } -} - -ExtensionsRegistry.registerExtensionPoint('commands', schema.commandContribution).setHandler(extensions => { - for (let extension of extensions) { - const {value} = extension; - if (Array.isArray(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); - }); - }; - } -} \ No newline at end of file diff --git a/src/vs/platform/actions/common/resourceContextKey.ts b/src/vs/platform/actions/common/resourceContextKey.ts new file mode 100644 index 00000000000..c82f16d3a6c --- /dev/null +++ b/src/vs/platform/actions/common/resourceContextKey.ts @@ -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 { + + static Scheme = 'resourceScheme'; + static LangId = 'resourceLangId'; + static Resource = 'resource'; + + private _resourceKey: IKeybindingContextKey; + private _schemeKey: IKeybindingContextKey; + private _langIdKey: IKeybindingContextKey; + + 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(); + } +} \ No newline at end of file diff --git a/src/vs/platform/actions/workbench/actionBarContributions.ts b/src/vs/platform/actions/workbench/actionBarContributions.ts deleted file mode 100644 index 725c6a4aab8..00000000000 --- a/src/vs/platform/actions/workbench/actionBarContributions.ts +++ /dev/null @@ -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(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 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} = this._action; - if (icon && element.classList.contains('icon')) { - element.style.backgroundImage = `url("${icon}")`; - } - } - - onClick(event: Event): void { - super.onClick(event); - } -} - -Registry.as(Extensions.Actionbar).registerActionBarContributor(Scope.EDITOR, EditorContributor); -Registry.as(Extensions.Actionbar).registerActionBarContributor(Scope.VIEWER, ContextMenuContributor); \ No newline at end of file diff --git a/src/vs/platform/actions/workbench/actionsService.ts b/src/vs/platform/actions/workbench/actionsService.ts deleted file mode 100644 index 70bf8e563db..00000000000 --- a/src/vs/platform/actions/workbench/actionsService.ts +++ /dev/null @@ -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); - } -} diff --git a/src/vs/platform/keybinding/browser/keybindingServiceImpl.ts b/src/vs/platform/keybinding/browser/keybindingServiceImpl.ts index 3ab0c2ebb50..6cf5ea85e9a 100644 --- a/src/vs/platform/keybinding/browser/keybindingServiceImpl.ts +++ b/src/vs/platform/keybinding/browser/keybindingServiceImpl.ts @@ -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 implements IKeybindingContextKey { export abstract class AbstractKeybindingService { public serviceId = IKeybindingService; + + protected _onDidChangeContext: Event; + protected _onDidChangeContextKey: Emitter; protected _myContextId: number; protected _instantiationService: IInstantiationService; constructor(myContextId: number) { this._myContextId = myContextId; + this._onDidChangeContextKey = new Emitter(); 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(domNode: HTMLElement, key: string): T; + + public get onDidChangeContext(): Event { + 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(domNode: HTMLElement, key: string): T { + const ctx = Object.create(null); + this.getContext(this._findContextAttr(domNode)).fillInContext(ctx); + this._configurationContext.fillInContext(ctx); + return 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, 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 { + return this._parent.onDidChangeContext; + } + + public contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean { + return this._parent.contextMatchesRules(domNode, rules); + } + + public getContextValue(domNode: HTMLElement, key: string): T { + return this._parent.getContextValue(domNode, key); + } + public getLabelFor(keybinding: Keybinding): string { return this._parent.getLabelFor(keybinding); } diff --git a/src/vs/platform/keybinding/common/keybindingService.ts b/src/vs/platform/keybinding/common/keybindingService.ts index 1e9db7a36cf..a9f14c80cb0 100644 --- a/src/vs/platform/keybinding/common/keybindingService.ts +++ b/src/vs/platform/keybinding/common/keybindingService.ts @@ -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('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; dispose(): void; + onDidChangeContext: Event; createKey(key: string, defaultValue: T): IKeybindingContextKey; + contextMatchesRules(domNode: IKeybindingScopeLocation, rules: KbExpr): boolean; + getContextValue(domNode: IKeybindingScopeLocation, key: string): T; createScoped(domNode: IKeybindingScopeLocation): IKeybindingService; diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index fe5185d9a45..e307b037d48 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -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 implements IKeybindingContextKey { private _key: string; @@ -39,6 +40,15 @@ export class MockKeybindingService implements IKeybindingService { public createKey(key: string, defaultValue: T): IKeybindingContextKey { return new MockKeybindingContextKey(key, defaultValue); } + public contextMatchesRules(domNode: HTMLElement, rules: KbExpr): boolean { + return false; + } + public get onDidChangeContext(): Event { + return Event.None; + } + public getContextValue(domNode: HTMLElement, key: string) { + return; + } public getLabelFor(keybinding: Keybinding): string { return keybinding._toUSLabel(); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 737653efed8..7d8ece69dc4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -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 => { diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 499a30b1c88..78a195821ad 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -56,6 +56,7 @@ export class NoTabsTitleControl extends TitleControl { } public create(parent: HTMLElement): void { + super.create(parent); this.titleContainer = parent; // Pin on double click diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts index be6d811fc4d..f2fa2c14800 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts @@ -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().workbench.editor.showTabs; - this.titleAreaControl[position] = useTabs ? this.instantiationService.createInstance(TabsTitleControl) : this.instantiationService.createInstance(NoTabsTitleControl); + this.titleAreaControl[position] = this.instantiationServices[position].createInstance(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]; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 687247fe1e1..5f4a15f2449 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -73,6 +73,7 @@ export class TabsTitleControl extends TitleControl { } public create(parent: HTMLElement): void { + super.create(parent); this.titleContainer = parent; // Tabs Container diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 68be8227519..45e00437110 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -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[] = []; diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 07d83498e13..f3716179ed9 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -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); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index e187c46a96f..cefbd9e34d4 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -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 ( input).getResource === 'function') { + let candidate = (input).getResource(); + if (candidate instanceof URI) { + return candidate; + } + } + return getUntitledOrFileResource(input, true); } /** diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index d7fa1ed06e7..e8eb8a93fd8 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -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; private explorerImportDelayer: ThrottledDelayer; + private resourceContext: ResourceContextKey; + private shouldRefresh: boolean; private autoReveal: boolean; @@ -96,6 +99,8 @@ export class ExplorerView extends CollapsibleViewletView { this.explorerRefreshDelayer = new ThrottledDelayer(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY); this.explorerImportDelayer = new ThrottledDelayer(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; } diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index efb5af4d1bb..65b1c120504 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -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: () => { diff --git a/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts b/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts index e242f1a020c..15333bc1ad3 100644 --- a/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts @@ -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 diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 099efea0993..69924242294 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -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';