diff --git a/src/vs/base/common/jsonEdit.ts b/src/vs/base/common/jsonEdit.ts index 8323f958dee..0af46c477fb 100644 --- a/src/vs/base/common/jsonEdit.ts +++ b/src/vs/base/common/jsonEdit.ts @@ -79,7 +79,20 @@ export function setProperty(text: string, path: JSONPath, value: any, formatting return withFormatting(text, edit, formattingOptions); } } else if (parent.type === 'array' && typeof lastSegment === 'number') { - throw new Error('Array modification not supported yet'); + let insertIndex = lastSegment; + if (insertIndex === -1) { + let newProperty = `${JSON.stringify(value)}`; + let edit: Edit; + if (parent.children.length === 0) { + edit = { offset: parent.offset + 1, length: 0, content: newProperty }; + } else { + let previous = parent.children[parent.children.length - 1]; + edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; + } + return withFormatting(text, edit, formattingOptions); + } else { + throw new Error('Array modification not supported yet'); + } } else { throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`); } diff --git a/src/vs/base/test/common/jsonEdit.test.ts b/src/vs/base/test/common/jsonEdit.test.ts index 26aab2f653b..9d16e2c6773 100644 --- a/src/vs/base/test/common/jsonEdit.test.ts +++ b/src/vs/base/test/common/jsonEdit.test.ts @@ -119,4 +119,16 @@ suite('JSON - edits', () => { edits = removeProperty(content, ['a'], formatterOptions); assertEdit(content, edits, '{\n "x": "y"\n}'); }); + + test('insert item to empty array', () => { + let content = '[\n]'; + let edits = setProperty(content, [-1], 'bar', formatterOptions); + assertEdit(content, edits, '[\n "bar"\n]'); + }); + + test('insert item', () => { + let content = '[\n 1,\n 2\n]'; + let edits = setProperty(content, [-1], 'bar', formatterOptions); + assertEdit(content, edits, '[\n 1,\n 2,\n "bar"\n]'); + }); }); \ No newline at end of file diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 722dd085ce7..26782016306 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -13,7 +13,7 @@ import Severity from 'vs/base/common/severity'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ICommandService, CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { KeybindingResolver, IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; -import { IKeybindingEvent, IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IKeybindingEvent, IKeybindingService, IKeybindingItem2, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IMessageService } from 'vs/platform/message/common/message'; @@ -169,6 +169,15 @@ export abstract class AbstractKeybindingService implements IKeybindingService { ); } + public getKeybindings(): IKeybindingItem2[] { + return this._getResolver().getKeybindings().map(keybinding => ({ + keybinding: this.resolveKeybinding(keybinding.keybinding), + command: keybinding.command, + when: keybinding.when, + source: keybinding.isDefault ? KeybindingSource.Default : KeybindingSource.User + })); + } + private static _getDefaultKeybindings(defaultKeybindings: NormalizedKeybindingItem[]): string { let out = new OutputBuilder(); out.writeLine('['); diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index ed7f6b4ac02..8ae7735ac3f 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -43,6 +43,13 @@ export interface IKeybindingItem { weight2: number; } +export interface IKeybindingItem2 { + keybinding: ResolvedKeybinding; + command: string; + source: KeybindingSource; + when: ContextKeyExpr; +} + export enum KeybindingSource { Default = 1, User @@ -64,6 +71,8 @@ export interface IKeybindingService { getDefaultKeybindings(): string; + getKeybindings(): IKeybindingItem2[]; + /** * @deprecated */ diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 7cbef446355..864799f47ac 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -16,6 +16,7 @@ export interface IResolveResult { export class KeybindingResolver { private readonly _defaultKeybindings: NormalizedKeybindingItem[]; + private readonly _keybindings: NormalizedKeybindingItem[]; private readonly _shouldWarnOnConflict: boolean; private readonly _defaultBoundCommands: Map; private readonly _map: Map; @@ -34,9 +35,9 @@ export class KeybindingResolver { this._map = new Map(); this._lookupMap = new Map(); - let allKeybindings = KeybindingResolver.combine(defaultKeybindings, overrides); - for (let i = 0, len = allKeybindings.length; i < len; i++) { - let k = allKeybindings[i]; + this._keybindings = KeybindingResolver.combine(defaultKeybindings, overrides); + for (let i = 0, len = this._keybindings.length; i < len; i++) { + let k = this._keybindings[i]; if (k.keypressFirstPart === null) { // unbound continue; @@ -204,6 +205,10 @@ export class KeybindingResolver { return this._defaultKeybindings; } + public getKeybindings(): NormalizedKeybindingItem[] { + return this._keybindings; + } + public lookupKeybindings(commandId: string): NormalizedKeybindingItem[] { let items = this._lookupMap.get(commandId); if (typeof items === 'undefined' || items.length === 0) { diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 2ef4f9aa3ae..645e12760fd 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -6,7 +6,7 @@ import { ResolvedKeybinding, Keybinding } from 'vs/base/common/keyCodes'; import Event from 'vs/base/common/event'; -import { IKeybindingService, IKeybindingEvent } from 'vs/platform/keybinding/common/keybinding'; +import { IKeybindingService, IKeybindingEvent, IKeybindingItem2 } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService, IContextKeyServiceTarget, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/abstractKeybindingService'; @@ -72,6 +72,10 @@ export class MockKeybindingService2 implements IKeybindingService { return null; } + public getKeybindings(): IKeybindingItem2[] { + return []; + } + public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding { return new USLayoutResolvedKeybinding(keybinding, OS); } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 5d8a3918fcd..4b36e94d751 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -52,6 +52,7 @@ import { IConfigurationEditingService } from 'vs/workbench/services/configuratio import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IActivityBarService } from 'vs/workbench/services/activity/common/activityBarService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -521,6 +522,9 @@ export class Workbench implements IPartService { this.configurationEditingService = this.instantiationService.createInstance(ConfigurationEditingService); serviceCollection.set(IConfigurationEditingService, this.configurationEditingService); + // Keybinding Editing + serviceCollection.set(IKeybindingEditingService, this.instantiationService.createInstance(KeybindingsEditingService)); + // Configuration Resolver const workspace = this.contextService.getWorkspace(); serviceCollection.set(IConfigurationResolverService, new SyncDescriptor(ConfigurationResolverService, workspace ? workspace.resource : null, process.env)); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts new file mode 100644 index 00000000000..c6ce1f9e052 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/keybindings'; +import * as nls from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import Event, { Emitter } from 'vs/base/common/event'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; +import * as dom from 'vs/base/browser/dom'; +import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; +import { renderHtml } from 'vs/base/browser/htmlContentRenderer'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Dimension } from 'vs/base/browser/builder'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +class KeybindingInputWidget extends Widget { + + public readonly inputBox: InputBox; + + private _onKeybinding = this._register(new Emitter()); + public readonly onKeybinding: Event = this._onKeybinding.event; + + private _onEnter = this._register(new Emitter()); + public readonly onEnter: Event = this._onEnter.event; + + private _onEscape = this._register(new Emitter()); + public readonly onEscape: Event = this._onEscape.event; + + constructor(parent: HTMLElement, private options: IInputOptions, + @IContextViewService private contextViewService: IContextViewService, + @IKeybindingService private keybindingService: IKeybindingService + ) { + super(); + this.inputBox = this._register(new InputBox(parent, this.contextViewService, this.options)); + this.onkeydown(this.inputBox.inputElement, e => this.onKeyDown(e)); + } + + private onKeyDown(keyboardEvent: IKeyboardEvent): void { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + switch (keyboardEvent.toKeybinding().value) { + case KeyCode.Enter: + this._onEnter.fire(); + return; + case KeyCode.Escape: + this._onEscape.fire(); + return; + } + this.printKeybinding(keyboardEvent); + } + + private printKeybinding(keyboardEvent: IKeyboardEvent): void { + const keybinding = this.keybindingService.resolveKeybinding(keyboardEvent.toKeybinding()); + this.inputBox.value = keybinding.getUserSettingsLabel().toLowerCase(); + this.inputBox.inputElement.title = 'keyCode: ' + keyboardEvent.browserEvent.keyCode; + this._onKeybinding.fire(keybinding); + } +} + +export class DefineKeybindingWidget extends Widget { + + private static WIDTH = 400; + private static HEIGHT = 90; + + private _domNode: FastDomNode; + private _keybindingInputWidget: KeybindingInputWidget; + private _outputNode: HTMLElement; + + private _resolvedKeybinding: ResolvedKeybinding = null; + private _isVisible: boolean = false; + + private _onHide = this._register(new Emitter()); + + constructor(parent: HTMLElement, + @IKeybindingService private keybindingService: IKeybindingService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(); + this.create(parent); + } + + define(): TPromise { + return new TPromise((c, e) => { + if (!this._isVisible) { + this._isVisible = true; + this._domNode.setDisplay('block'); + + this._resolvedKeybinding = null; + this._keybindingInputWidget.inputBox.value = ''; + dom.clearNode(this._outputNode); + this._keybindingInputWidget.inputBox.focus(); + } + const disposable = this._onHide.event(() => { + if (this._resolvedKeybinding) { + c(this._resolvedKeybinding.getUserSettingsLabel()); + } else { + c(null); + } + disposable.dispose(); + }); + }); + } + + layout(layout: Dimension): void { + let top = Math.round((layout.height - DefineKeybindingWidget.HEIGHT) / 2); + this._domNode.setTop(top); + + let left = Math.round((layout.width - DefineKeybindingWidget.WIDTH) / 2); + this._domNode.setLeft(left); + } + + private create(parent: HTMLElement): void { + this._domNode = createFastDomNode(document.createElement('div')); + this._domNode.setDisplay('none'); + this._domNode.setClassName('defineKeybindingWidget'); + this._domNode.setWidth(DefineKeybindingWidget.WIDTH); + this._domNode.setHeight(DefineKeybindingWidget.HEIGHT); + + dom.append(parent, this._domNode.domNode); + dom.append(this._domNode.domNode, dom.$('.message', null, nls.localize('defineKeybinding.initial', "Press desired key combination and ENTER. ESCAPE to cancel."))); + + this._keybindingInputWidget = this.instantiationService.createInstance(KeybindingInputWidget, this._domNode.domNode, {}); + this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.printKeybinding(keybinding))); + this._register(this._keybindingInputWidget.onEnter(() => this.hide())); + this._register(this._keybindingInputWidget.onEscape(() => this.onCancel())); + this._register(dom.addDisposableListener(this._keybindingInputWidget.inputBox.inputElement, 'blur', e => this.onCancel())); + + this._outputNode = dom.append(this._domNode.domNode, dom.$('.output'));; + } + + private printKeybinding(keybinding: ResolvedKeybinding): void { + this._resolvedKeybinding = keybinding; + dom.clearNode(this._outputNode); + let htmlkb = this._resolvedKeybinding.getHTMLLabel(); + htmlkb.forEach((item) => this._outputNode.appendChild(renderHtml(item))); + } + + private onCancel(): void { + this._resolvedKeybinding = null; + this.hide(); + } + + private hide(): void { + this._isVisible = false; + this._onHide.fire(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts new file mode 100644 index 00000000000..c7a91ac0a65 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -0,0 +1,574 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/keybindingsEditor'; +import { localize } from 'vs/nls'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Delayer } from 'vs/base/common/async'; +import * as DOM from 'vs/base/browser/dom'; +import { Builder, Dimension } from 'vs/base/browser/builder'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IAction } from 'vs/base/common/actions'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { KeybindingsEditorModel, IKeybindingItemEntry, IKeybindingItem } from 'vs/workbench/parts/preferences/common/keybindingsEditorModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService, IKeybindingItem2, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { SearchWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { DefineKeybindingWidget } from 'vs/workbench/parts/preferences/browser/keybindingWidgets'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { renderHtml } from 'vs/base/browser/htmlContentRenderer'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +let $ = DOM.$; + +export interface IKeybindingsEditor { + + defineKeybinding(keybindingItem: IKeybindingItem); + +} + +export class KeybindingsEditorInput extends EditorInput { + + public static ID: string = 'worknench.input.keybindings'; + public readonly keybindingsModel: KeybindingsEditorModel; + + constructor( @IInstantiationService private instantiationService: IInstantiationService) { + super(); + this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel); + } + + getTypeId(): string { + return KeybindingsEditorInput.ID; + } + + getName(): string { + return localize('keybindingsInputName', "Keybindings"); + } + + resolve(refresh?: boolean): TPromise { + return TPromise.as(this.keybindingsModel); + } + + matches(otherInput: any): boolean { + return otherInput instanceof KeybindingsEditorInput; + } +} + +export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor { + + public static ID: string = 'workbench.editor.keybindings'; + private keybindingsContentElement: HTMLElement; + private scrollableElement: DomScrollableElement; + private searchWidget: SearchWidget; + private defineKeybindingWidget: DefineKeybindingWidget; + private overlayContainer: HTMLElement; + private dimension: Dimension; + + private activeKeybindingData: IKeybindingItemEntry; + private activeKeybindingRow: HTMLElement; + + private delayedFiltering: Delayer; + private keybindingsList: List; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IKeybindingService private keybindingsService: IKeybindingService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IKeybindingEditingService private keybindingEditingService: IKeybindingEditingService, + @IListService private listService: IListService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(KeybindingsEditor.ID, telemetryService, themeService); + this.delayedFiltering = new Delayer(300); + this._register(keybindingsService.onDidUpdateKeybindings(() => this.render())); + } + + createEditor(parent: Builder): void { + const parentElement = parent.getHTMLElement(); + + const keybindingsEditorElement = DOM.append(parentElement, $('div', { class: 'keybindings-editor' })); + + this.createOverlayContainer(keybindingsEditorElement); + this.createHeader(keybindingsEditorElement); + this.createBody(keybindingsEditorElement); + } + + setInput(input: KeybindingsEditorInput): TPromise { + const oldInput = this.input; + return super.setInput(input) + .then(() => { + if (!input.matches(oldInput)) { + this.render(); + } + }); + } + + clearInput(): void { + super.clearInput(); + this.keybindingsContentElement.removeChild(this.keybindingsContentElement.children.item(0)); + this.searchWidget.clear(); + } + + layout(dimension: Dimension): void { + this.dimension = dimension; + this.searchWidget.layout(dimension); + + this.overlayContainer.style.width = dimension.width + 'px'; + this.overlayContainer.style.height = dimension.height + 'px'; + if (this.scrollableElement) { + this.scrollableElement.scanDomNode(); + } + this.defineKeybindingWidget.layout(this.dimension); + if (this.keybindingsList) { + this.keybindingsList.layout(dimension.height); + } + } + + focus(): void { + this.searchWidget.focus(); + } + + private createOverlayContainer(parent: HTMLElement): void { + this.overlayContainer = DOM.append(parent, $('.overlay-container')); + this.overlayContainer.style.position = 'absolute'; + this.overlayContainer.style.display = 'none'; + this.overlayContainer.style.zIndex = '10'; + this.defineKeybindingWidget = this._register(this.instantiationService.createInstance(DefineKeybindingWidget, this.overlayContainer)); + } + + private createHeader(parent: HTMLElement): void { + const headerContainer = DOM.append(parent, $('.keybindings-header')); + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, DOM.append(headerContainer, $('.search-container')), { + ariaLabel: localize('SearchKeybindings.AriaLabel', "Search keybindings"), + placeholder: localize('SearchKeybindings.Placeholder', "Search keybindings") + })); + this._register(this.searchWidget.onDidChange(searchValue => this.delayedFiltering.trigger(() => this.render()))); + } + + private createBody(parent: HTMLElement): void { + const bodyContainer = DOM.append(parent, $('.keybindings-body')); + + const scrollContainer = $('.keybindings-scroll-container'); + this.scrollableElement = new DomScrollableElement(scrollContainer, { canUseTranslate3d: false }); + this.scrollableElement.scanDomNode(); + DOM.append(bodyContainer, this.scrollableElement.getDomNode()); + + const openKeybindingsContainer = DOM.append(scrollContainer, $('.open-keybindings-container')); + DOM.append(openKeybindingsContainer, $('span', null, localize('header-message', "For advanced customizations open and edit "))); + const fileElement = DOM.append(openKeybindingsContainer, $('span.file-name', null, localize('keybindings-file-name', "keybindings.json"))); + this._register(DOM.addDisposableListener(fileElement, DOM.EventType.CLICK, () => this.editorService.openEditor({ resource: URI.file(this.environmentService.appKeybindingsPath), options: { pinned: true } }))); + + this.keybindingsContentElement = DOM.append(scrollContainer, $('.content')); + } + + /*private createBody(parent: HTMLElement): void { + const bodyContainer = DOM.append(parent, $('.keybindings-body')); + + const openKeybindingsContainer = DOM.append(bodyContainer, $('.open-keybindings-container')); + DOM.append(openKeybindingsContainer, $('span', null, localize('header-message', "For advanced customizations open and edit "))); + const fileElement = DOM.append(openKeybindingsContainer, $('span.file-name', null, localize('keybindings-file-name', "keybindings.json"))); + this._register(DOM.addDisposableListener(fileElement, DOM.EventType.CLICK, () => this.editorService.openEditor({ resource: URI.file(this.environmentService.appKeybindingsPath), options: { pinned: true } }))); + + this.createList(bodyContainer); + } + + private createList(parent: HTMLElement): void { + const delegate = new Delegate(); + this.createListHeader(parent); + const keybindingListContainer = DOM.append(parent, $('.keybindings-list-container')); + this.keybindingsList = this._register(new List(keybindingListContainer, delegate, [new KeybindingItemRenderer(this)], { identityProvider: e => e.id })); + this._register(this.listService.register(this.keybindingsList)); + } + + private createListHeader(parent: HTMLElement): void { + DOM.append(parent, $('.keybindings-list-header', null, + $('.header.actions'), + $('.header.command', null, localize('command', "Command")), + $('.header.keybinding', null, localize('keybinding', "Keybinding")), + $('.header.source', null, localize('source', "Source")), + $('.header.when', null, localize('when', "When")))); + }*/ + + private render(): TPromise { + if (this.input) { + return this.input.resolve() + .then((keybindingsModel: KeybindingsEditorModel) => keybindingsModel.resolve() + .then(() => this.renderKeybindingsData(keybindingsModel.fetch(this.searchWidget.value())))); + } + return TPromise.as(null); + } + + private renderKeybindingsData(keybindingsData: IKeybindingItemEntry[]): void { + if (this.keybindingsList) { + this.keybindingsList.splice(0, this.keybindingsList.length, keybindingsData); + } else { + if (this.keybindingsContentElement.children.item(0)) { + this.keybindingsContentElement.removeChild(this.keybindingsContentElement.children.item(0)); + } + DOM.append(this.keybindingsContentElement, $('div', null, this.renderKeybindingsGroup('', keybindingsData))); + this.scrollableElement.scanDomNode(); + } + } + + private renderKeybindingsGroup(groupName: string, keybindingsEntries: IKeybindingItemEntry[]): HTMLElement { + if (keybindingsEntries.length === 0) { + return null; + } + + return $('table', null, + $('tr', null, + $('th.actions'), + $('th.command', null, localize('command', "Command")), + $('th.keybinding', null, localize('keybinding', "Keybinding")), + $('th.source', null, localize('source', "Source")), + $('th.when', null, localize('when', "When")), + ), + ...keybindingsEntries.map(keybindingData => this.renderKeybindingEntry(keybindingData))); + } + + private renderKeybindingEntry(keybindingData: IKeybindingItemEntry): HTMLElement { + const keybindingEntryRow = $('tr', { 'tabindex': '0' }); + + const actionsColumn = DOM.append(keybindingEntryRow, this.renderActionsColumn(keybindingData.keybindingItem)); + DOM.append(keybindingEntryRow, this.renderCommandColumn(keybindingData)); + DOM.append(keybindingEntryRow, this.renderKeybindingColumn(keybindingData)); + DOM.append(keybindingEntryRow, this.renderSourceColumn(keybindingData.keybindingItem)); + DOM.append(keybindingEntryRow, this.renderWhenColumn(keybindingData.keybindingItem)); + + const focusTracker = this._register(DOM.trackFocus(keybindingEntryRow)); + this._register(focusTracker.addFocusListener(() => this.onKeybindingRowFocussed(keybindingEntryRow, keybindingData))); + this._register(focusTracker.addBlurListener(() => this.onKeybindingRowBlurred(keybindingEntryRow))); + + this._register(DOM.addDisposableListener(keybindingEntryRow, DOM.EventType.KEY_DOWN, e => this.onkeydown(new StandardKeyboardEvent(e), keybindingEntryRow))); + this._register(DOM.addDisposableListener(keybindingEntryRow, DOM.EventType.MOUSE_MOVE, e => this.activeKeybindingRow ? DOM.removeClass(this.activeKeybindingRow, 'focussed') : null)); + this._register(DOM.addDisposableListener(keybindingEntryRow, DOM.EventType.CONTEXT_MENU, (e) => this.renderContextMenu(e, actionsColumn, keybindingData.keybindingItem))); + + return keybindingEntryRow; + } + + private onKeybindingRowFocussed(keybindingRow: HTMLElement, keybindingData: IKeybindingItemEntry): void { + DOM.addClass(keybindingRow, 'focussed'); + this.activeKeybindingRow = keybindingRow; + this.activeKeybindingData = keybindingData; + } + + private onKeybindingRowBlurred(keybindingRow: HTMLElement): void { + if (keybindingRow === this.activeKeybindingRow) { + this.activeKeybindingData = null; + this.activeKeybindingRow = null; + } + DOM.removeClass(keybindingRow, 'focussed'); + } + + private onkeydown(keyboardEvent: StandardKeyboardEvent, rowElement: HTMLElement): void { + let handled = false; + switch (keyboardEvent.keyCode) { + case KeyCode.DownArrow: + handled = this.focusKeybindingRowSibling(rowElement, true); + break; + case KeyCode.UpArrow: + handled = this.focusKeybindingRowSibling(rowElement, false); + break; + case KeyCode.Tab: + if (keyboardEvent.shiftKey) { + handled = this.focusKeybindingRowSibling(rowElement, false); + } else { + handled = this.focusKeybindingRowSibling(rowElement, true); + } + break; + } + if (handled) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + } + } + + private focusKeybindingRowSibling(element: HTMLElement, next: boolean): boolean { + const elementToFocus = (next ? element.nextSibling : element.previousSibling); + if (elementToFocus && element.parentNode.firstChild !== elementToFocus) { + elementToFocus.focus(); + return true; + } + return false; + } + + private renderContextMenu(e: any, element: HTMLElement, keybindingItem: IKeybindingItem): void { + let anchor: HTMLElement | { x: number, y: number } = element; + if (event instanceof MouseEvent) { + const event = new StandardMouseEvent(e); + anchor = { x: event.posx, y: event.posy }; + } + const actions = [this.createRemoveAction(keybindingItem)]; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => TPromise.wrap(actions) + }); + } + + private renderActionsColumn(keybindingEntry: IKeybindingItem): HTMLElement { + const actionsContainer = $('td.actions'); + const actionbar = new ActionBar(actionsContainer, { animated: false }); + const actions = []; + if (keybindingEntry.keybinding) { + actions.push(this.createEditAction(keybindingEntry)); + } else { + actions.push(this.createAddAction(keybindingEntry)); + } + actionbar.push(actions, { icon: true }); + return actionsContainer; + } + + private renderCommandColumn(keybindingData: IKeybindingItemEntry): HTMLElement { + const commandColumn = $('td.command', null); + const keybindingItem = keybindingData.keybindingItem; + if (keybindingItem.commandLabel) { + new HighlightedLabel(commandColumn).set(keybindingItem.commandLabel, keybindingData.commandLabelMatches); + } + new HighlightedLabel(DOM.append(commandColumn, $('.code.strong'))).set(keybindingItem.command, keybindingData.commandIdMatches); + return commandColumn; + } + + private renderKeybindingColumn(keybindingData: IKeybindingItemEntry): HTMLElement { + const keybindingColumn = $('td.keybinding'); + if (keybindingData.keybindingItem.keybinding) { + const htmlkbELement = DOM.append(keybindingColumn, $('.htmlkb')); + let htmlkb = keybindingData.keybindingItem.keybinding.getHTMLLabel(); + htmlkb.forEach(item => htmlkbELement.appendChild(renderHtml(item))); + new HighlightedLabel(DOM.append(keybindingColumn, $('.code'))).set(keybindingData.keybindingItem.keybinding.getAriaLabel(), keybindingData.keybindingMatches); + } else { + DOM.append(keybindingColumn, $('.empty', null, '—')); + } + return keybindingColumn; + } + + private renderSourceColumn(keybindingItem: IKeybindingItem): HTMLElement { + return $('td.source', null, keybindingItem.source === KeybindingSource.User ? localize('user', "User") : localize('default', "Default")); + } + + private renderWhenColumn(keybindingItem: IKeybindingItem): HTMLElement { + return $('td.when', null, keybindingItem.when ? $('.code', null, keybindingItem.when.serialize()) : $('.empty', null, '—')); + } + + private createEditAction(keybinding: IKeybindingItem): IAction { + return { + class: 'edit', + enabled: true, + id: 'editKeybinding', + tooltip: localize('change', "Change Keybinding"), + run: () => this.defineKeybinding(keybinding) + }; + } + + private createRemoveAction(keybinding: IKeybindingItem): IAction { + return { + label: localize('removeLabel', "Remove Keybinding"), + enabled: !!keybinding.keybinding, + id: 'removeKeybinding', + run: () => this.keybindingEditingService.removeKeybinding(keybinding) + }; + } + + private createAddAction(keybinding: IKeybindingItem): IAction { + return { + class: 'add', + enabled: true, + id: 'addKeybinding', + tooltip: localize('add', "Add Keybinding"), + run: () => this.defineKeybinding(keybinding) + }; + } + + defineKeybinding(keybindingItem: IKeybindingItem2): void { + this.overlayContainer.style.display = 'block'; + this.defineKeybindingWidget.define().then(key => { + this.overlayContainer.style.display = 'none'; + if (key) { + this.keybindingEditingService.editKeybinding(key, keybindingItem); + } + }, () => { + this.overlayContainer.style.display = 'none'; + this.focus(); + }); + } +} + +class Delegate implements IDelegate { + + getHeight() { return 24; } + + getTemplateId(element: IKeybindingItemEntry) { + return KeybindingItemRenderer.TEMPLATE_ID; + } +} + +interface KeybindingItemTemplate { + actions: ActionsColumn; + command: CommandColumn; + keybinding: KeybindingColumn; + source: SourceColumn; + when: WhenColumn; +} + +class Column { + constructor(protected parent: HTMLElement, protected keybindingsEditor: IKeybindingsEditor) { + this.create(parent); + } + create(parent: HTMLElement) { } +} + +class ActionsColumn extends Column { + + private actionBar: ActionBar; + + create(parent: HTMLElement) { + const actionsContainer = DOM.append(parent, $('.column.actions')); + this.actionBar = new ActionBar(actionsContainer, { animated: false }); + } + + render(keybindingItemEntry: IKeybindingItemEntry): void { + this.actionBar.clear(); + const actions = []; + if (keybindingItemEntry.keybindingItem.keybinding) { + actions.push(this.createEditAction(keybindingItemEntry.keybindingItem)); + } else { + actions.push(this.createAddAction(keybindingItemEntry.keybindingItem)); + } + this.actionBar.push(actions, { icon: true }); + } + + private createEditAction(keybinding: IKeybindingItem): IAction { + return { + class: 'edit', + enabled: true, + id: 'editKeybinding', + tooltip: localize('change', "Change Keybinding"), + run: () => this.keybindingsEditor.defineKeybinding(keybinding) + }; + } + + private createAddAction(keybinding: IKeybindingItem): IAction { + return { + class: 'add', + enabled: true, + id: 'addKeybinding', + tooltip: localize('add', "Add Keybinding"), + run: () => this.keybindingsEditor.defineKeybinding(keybinding) + }; + } +} + +class CommandColumn extends Column { + + private commandColumn: HTMLElement; + + create(parent: HTMLElement) { + this.commandColumn = DOM.append(parent, $('.column.command')); + } + + render(keybindingItemEntry: IKeybindingItemEntry): void { + DOM.clearNode(this.commandColumn); + + const keybindingItem = keybindingItemEntry.keybindingItem; + if (keybindingItem.commandLabel) { + new HighlightedLabel(this.commandColumn).set(keybindingItem.commandLabel, keybindingItemEntry.commandLabelMatches); + } + new HighlightedLabel(DOM.append(this.commandColumn, $('.code.strong'))).set(keybindingItem.command, keybindingItemEntry.commandIdMatches); + } +} + +class KeybindingColumn extends Column { + + private keybindingColumn: HTMLElement; + + create(parent: HTMLElement) { + this.keybindingColumn = DOM.append(parent, $('.column.keybinding')); + } + + render(keybindingItemEntry: IKeybindingItemEntry): void { + DOM.clearNode(this.keybindingColumn); + if (keybindingItemEntry.keybindingItem.keybinding) { + let keybinding = DOM.append(this.keybindingColumn, $('.htmlkb')); + let htmlkb = keybindingItemEntry.keybindingItem.keybinding.getHTMLLabel(); + htmlkb.forEach(item => keybinding.appendChild(renderHtml(item))); + new HighlightedLabel(DOM.append(this.keybindingColumn, $('.code'))).set(keybindingItemEntry.keybindingItem.keybinding.getAriaLabel(), keybindingItemEntry.keybindingMatches); + } + } +} + +class SourceColumn extends Column { + + private sourceColumn: HTMLElement; + + create(parent: HTMLElement) { + this.sourceColumn = DOM.append(parent, $('.column.source')); + } + + render(keybindingItemEntry: IKeybindingItemEntry): void { + this.sourceColumn.textContent = keybindingItemEntry.keybindingItem.source === KeybindingSource.User ? localize('user', "User") : localize('default', "Default"); + } +} + +class WhenColumn extends Column { + + private whenColumn: HTMLElement; + + create(parent: HTMLElement) { + const column = DOM.append(parent, $('.column.when')); + this.whenColumn = DOM.append(column, $('div')); + } + + render(keybindingItemEntry: IKeybindingItemEntry): void { + DOM.toggleClass(this.whenColumn, 'code', !!keybindingItemEntry.keybindingItem.when); + DOM.toggleClass(this.whenColumn, 'empty', !keybindingItemEntry.keybindingItem.when); + this.whenColumn.textContent = keybindingItemEntry.keybindingItem.when ? keybindingItemEntry.keybindingItem.when.serialize() : '—'; + } +} + +class KeybindingItemRenderer implements IRenderer { + + static TEMPLATE_ID = 'keybinding_item_template'; + get templateId(): string { return KeybindingItemRenderer.TEMPLATE_ID; } + + constructor(private keybindingsEditor: IKeybindingsEditor) { } + + renderTemplate(container: HTMLElement): KeybindingItemTemplate { + return { + actions: new ActionsColumn(container, this.keybindingsEditor), + command: new CommandColumn(container, this.keybindingsEditor), + keybinding: new KeybindingColumn(container, this.keybindingsEditor), + source: new SourceColumn(container, this.keybindingsEditor), + when: new WhenColumn(container, this.keybindingsEditor) + }; + } + + renderElement(keybindingEntry: IKeybindingItemEntry, index: number, template: KeybindingItemTemplate): void { + + template.actions.render(keybindingEntry); + template.command.render(keybindingEntry); + template.keybinding.render(keybindingEntry); + template.source.render(keybindingEntry); + template.when.render(keybindingEntry); + } + + disposeTemplate(template: KeybindingItemTemplate): void { + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/add.svg b/src/vs/workbench/parts/preferences/browser/media/add.svg new file mode 100644 index 00000000000..bdecdb0e45b --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/add.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/add_inverse.svg b/src/vs/workbench/parts/preferences/browser/media/add_inverse.svg new file mode 100644 index 00000000000..3475c1e1963 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/add_inverse.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/keybindings.css b/src/vs/workbench/parts/preferences/browser/media/keybindings.css new file mode 100644 index 00000000000..83c50ada73c --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/keybindings.css @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.keybindings-editor .defineKeybindingWidget { + padding: 10px; + position: absolute; +} + +.keybindings-editor .defineKeybindingWidget .message { + width: 400px; + text-align: center; + white-space: nowrap; +} + +.keybindings-editor .defineKeybindingWidget .monaco-inputbox, +.keybindings-editor .defineKeybindingWidget .output { + margin-top:10px; + display: block; + text-align: center; +} + +/* Theming */ +.keybindings-editor .defineKeybindingWidget { + background-color: #EFEFF2; + box-shadow: 0 2px 8px #A8A8A8; +} + +.vs-dark .keybindings-editor .defineKeybindingWidget { + background-color: #2D2D30; + box-shadow: 0 2px 8px #000; +} + +.keybindings-editor .inlineKeybindingInfo:before { + margin: 0.2em 0.1em 0 0.1em; + content:" "; + display:inline-block; + height:0.8em; + width:1em; + background: url(info.svg) 0px -0.1em no-repeat; + background-size: 0.9em; +} + +.keybindings-editor .inlineKeybindingError:before { + margin: 0.1em 0.1em 0 0.1em; + content:" "; + display:inline-block; + height:0.8em; + width:1em; + background: url(status-error.svg) 0px -0.1em no-repeat; + background-size: 1em; +} + +.keybindings-editor .keybindingInfo { + box-shadow: inset 0 0 0 1px #B9B9B9; + background-color: rgba(100, 100, 250, 0.2); +} + +.keybindings-editor .keybindingError { + box-shadow: inset 0 0 0 1px #B9B9B9; + background-color: rgba(250, 100, 100, 0.2); +} + +/* for keybindings rendered as HTML */ +/* for keybindings rendered as HTML */ +.monaco-kb { + white-space: nowrap; +} +.monaco-kbkey { + display: inline-block; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; + background-color: #ddd; + vertical-align: middle; + color: #555; + line-height: 10px; + font-size: 11px; + padding: 3px 5px; +} diff --git a/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css new file mode 100644 index 00000000000..2ac72365a25 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css @@ -0,0 +1,364 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.keybindings-editor { + padding: 11px 0px 0px 27px; +} + +.keybindings-editor, +.keybindings-editor > .keybindings-body { + height: 100%; +} + +/* header styling */ + +.keybindings-editor > .keybindings-header { + padding: 0px 10px 11px 0; + border-bottom: 1px solid #efeff2; + display: flex; + flex-wrap: wrap; +} + +.vs-dark .keybindings-editor > .keybindings-header { + border-bottom: 1px solid #2d2d2d; +} + +.keybindings-editor > .keybindings-header .search-container { + flex: 1; +} + +.keybindings-editor > .keybindings-header .search-container > .settings-search-input { + vertical-align: middle; +} + +.keybindings-editor > .keybindings-header .search-container > .settings-search-input > .monaco-inputbox { + height: 30px; +} + +.keybindings-editor > .keybindings-header .search-container > .settings-search-input > .monaco-inputbox { + border: 1px solid #ddd; +} + +.keybindings-editor > .keybindings-header .search-container > .settings-search-input > .monaco-inputbox .input { + font-size: 14px; + padding-left:10px; +} + +/* body styling */ + +.keybindings-editor > .keybindings-body .open-keybindings-container { + margin-top: 10px; + color: #aaa; +} + +.keybindings-editor > .keybindings-body .open-keybindings-container > .file-name { + text-decoration: underline; + cursor: pointer; +} + +.keybindings-editor > .keybindings-body > .monaco-scrollable-element { + height: 100%; + overflow: hidden; +} + +.keybindings-editor > .keybindings-body > .monaco-scrollable-element > .keybindings-scroll-container { + height: 100%; + overflow-y: scroll; + box-sizing: border-box; + margin-right: 10px; +} + +.keybindings-editor > .keybindings-body .content details > summary { + cursor: pointer; + margin-bottom: 10px; + font-weight: bold; + font-size: 120%; + border-bottom: 1px solid rgba(128, 128, 128, 0.22); + padding: 6px 4px; +} + +.keybindings-editor > .keybindings-body .content table { + width: 100%; + border-spacing: 0; + border-collapse: separate; + margin-top: 10px; +} + +.keybindings-editor > .keybindings-body .content table tr { + display: flex; +} + +.keybindings-editor > .keybindings-body .content table tr:focus { + outline: none; +} + +.keybindings-editor > .keybindings-body .content table tr:nth-child(odd) { + background-color: rgba(130, 130, 130, 0.04); +} + +.keybindings-editor > .keybindings-body .content details > summary:focus, +.keybindings-editor > .keybindings-body .content table tr:not(:first-child).focussed, +.keybindings-editor > .keybindings-body .content table tr:not(:first-child):hover { + background-color: rgba(128, 128, 128, 0.15); +} + +.keybindings-editor > .keybindings-body .content .empty-message { + margin-left: 40px; +} + +.keybindings-editor > .keybindings-body .content table th, +.keybindings-editor > .keybindings-body .content table td { + padding: 2px 16px 2px 4px; + align-items: center; + display: flex; + overflow: hidden; +} + +.keybindings-editor > .keybindings-body .content table th.actions, +.keybindings-editor > .keybindings-body .content table td.actions { + width: 25px; + padding: 0px; +} + +.keybindings-editor > .keybindings-body .content table th.command, +.keybindings-editor > .keybindings-body .content table td.command { + flex: 2.5; + flex-wrap: wrap; +} + +.keybindings-editor > .keybindings-body .content table th.keybinding, +.keybindings-editor > .keybindings-body .content table td.keybinding { + flex: 2; +} + +.keybindings-editor > .keybindings-body .content table th.when, +.keybindings-editor > .keybindings-body .content table td.when { + flex: 2; +} + +.keybindings-editor > .keybindings-body .content table th.source, +.keybindings-editor > .keybindings-body .content table td.source { + flex: 0.5; +} + +.keybindings-editor > .keybindings-body .content table th:last-child, +.keybindings-editor > .keybindings-body .content table td:last-child { + padding: 2px 4px; +} + +.keybindings-editor > .keybindings-body .content table th { + text-align: left; +} + +.keybindings-editor > .keybindings-body .content details { + margin-top: 20px; +} + +.keybindings-editor > .keybindings-body .content table td .empty { + padding-left: 4px; +} + +.keybindings-editor > .keybindings-body .content table > tr > td > .code { + font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 90%; + padding: 1px 4px; + opacity: 0.8; +} + +.keybindings-editor > .keybindings-body .content table > tr > td > .code.strong { + background-color: rgba(128, 128, 128, 0.17); + border-radius: 4px; +} + +.keybindings-editor > .keybindings-body .content table > tr > td > .code:not(:first-child) { + margin-left: 0.5em; +} + +.keybindings-editor > .keybindings-body .content table td .highlight { + color: #007ACC; + font-weight: bold; +} + +.vs-dark .keybindings-editor > .keybindings-body .content table td .highlight { + color: #0097FB; +} + +.keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar { + display: none; + flex: 1; +} + +.keybindings-editor > .keybindings-body .content table > tr.focussed > td.actions .monaco-action-bar, +.keybindings-editor > .keybindings-body .content table > tr:hover > td .monaco-action-bar { + display: flex; +} + +.keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon { + width:16px; + height: 16px; + cursor: pointer; + margin-right: 4px; +} + +.keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.edit { + background: url('edit.svg') center center no-repeat; + transform: rotate(-90deg); +} + +.hc-black .keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.edit, +.vs-dark .keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.edit { + background: url('edit_inverse.svg') center center no-repeat; +} + +.keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.add { + background: url('add.svg') center center no-repeat; +} + +.hc-black .keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.add, +.vs-dark .keybindings-editor > .keybindings-body .content table > tr > td .monaco-action-bar .action-item > .icon.add { + background: url('add_inverse.svg') center center no-repeat; +} + + +/** List based styling **/ + +/* +.keybindings-editor > .keybindings-body > .keybindings-list-header, +.keybindings-editor > .keybindings-body .keybindings-list-container { + width: 100%; + border-spacing: 0; + border-collapse: separate; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header { + margin-top: 10px; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row { + display: flex; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row:focus { + outline:none; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row:nth-child(odd) { + background-color: rgba(130, 130, 130, 0.04); +} + +.keybindings-editor > .keybindings-body .keybindings-list-container .monaco-list-row:not(:first-child).focussed, +.keybindings-editor > .keybindings-body .keybindings-list-container .monaco-list-row:not(:first-child):hover { + background-color: rgba(128, 128, 128, 0.15); +} + +.keybindings-editor > .keybindings-body .keybindings-list-header > .header:last-child, +.keybindings-editor > .keybindings-body .keybindings-list-container .monaco-list-row > .column:last-child { + padding: 2px 4px; +} + +.keybindings-editor > .keybindings-body .keybindings-list-header > .header { + text-align: left; + font-weight: bold; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .header, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .column { + padding: 2px 16px 2px 4px; + align-items: center; + display: flex; + overflow: hidden; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .actions, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .actions { + width: 25px; + padding: 0px; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .command, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .command { + flex: 3; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .keybinding, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .keybinding { + flex: 2; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .source, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .source { + flex: 0.5; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-header .when, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row .when { + flex: 1; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .when .empty { + padding-left: 4px; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column > .code { + font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 90%; + padding: 1px 4px; + opacity: 0.8; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column > .code.strong { + background-color: rgba(128, 128, 128, 0.17); + border-radius: 4px; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column > .code:not(:first-child) { + margin-left: 0.5em; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .highlight { + color: #007ACC; + font-weight: bold; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .highlight { + color: #0097FB; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar { + display: none; + flex: 1; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row.focussed > .column.actions .monaco-action-bar, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row:hover > .column.actions .monaco-action-bar { + display: flex; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon { + width:16px; + height: 16px; + cursor: pointer; + margin-right: 4px; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.edit { + background: url('edit.svg') center center no-repeat; + transform: rotate(-90deg); +} + +.hc-black .keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.edit, +.vs-dark .keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.edit { + background: url('edit_inverse.svg') center center no-repeat; +} + +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.add { + background: url('add.svg') center center no-repeat; +} + +.hc-black .keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.add, +.vs-dark .keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar .action-item > .icon.add { + background: url('add_inverse.svg') center center no-repeat; +}*/ diff --git a/src/vs/workbench/parts/preferences/browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/browser/preferences.contribution.ts index 82ff04b094b..5cb6a24f29b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/browser/preferences.contribution.ts @@ -15,6 +15,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { DefaultPreferencesEditorInput, PreferencesEditor, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; +import { KeybindingsEditor, KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; import { OpenGlobalSettingsAction, OpenGlobalKeybindingsAction, OpenWorkspaceSettingsAction, ConfigureLanguageBasedSettingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions'; import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; import { PreferencesService } from 'vs/workbench/parts/preferences/browser/preferencesService'; @@ -36,6 +37,18 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); +Registry.as(EditorExtensions.Editors).registerEditor( + new EditorDescriptor( + KeybindingsEditor.ID, + nls.localize('keybindingsEditor', "Keybindings Editor"), + 'vs/workbench/parts/preferences/browser/keybindingsEditor', + 'KeybindingsEditor' + ), + [ + new SyncDescriptor(KeybindingsEditorInput) + ] +); + interface ISerializedPreferencesEditorInput { name: string; description: string; @@ -98,6 +111,21 @@ class PreferencesEditorInputFactory implements IEditorInputFactory { } } +class KeybindingsEditorInputFactory implements IEditorInputFactory { + + public serialize(editorInput: EditorInput): string { + const input = editorInput; + return JSON.stringify({ + name: input.getName(), + typeId: input.getTypeId() + }); + } + + public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + return instantiationService.createInstance(KeybindingsEditorInput); + } +} + interface ISerializedDefaultPreferencesEditorInput { resource: string; @@ -123,6 +151,7 @@ class DefaultPreferencesEditorInputFactory implements IEditorInputFactory { Registry.as(EditorExtensions.Editors).registerEditorInputFactory(PreferencesEditorInput.ID, PreferencesEditorInputFactory); Registry.as(EditorExtensions.Editors).registerEditorInputFactory(DefaultPreferencesEditorInput.ID, DefaultPreferencesEditorInputFactory); +Registry.as(EditorExtensions.Editors).registerEditorInputFactory(KeybindingsEditorInput.ID, KeybindingsEditorInputFactory); // Contribute Global Actions const category = nls.localize('preferences', "Preferences"); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 05ad3b5669d..f2609212c2e 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -123,7 +123,10 @@ export class PreferencesEditor extends BaseEditor { this.headerContainer = DOM.append(parentElement, DOM.$('.preferences-header')); - this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.headerContainer)); + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.headerContainer, { + ariaLabel: nls.localize('SearchSettingsWidget.AriaLabel', "Search settings"), + placeholder: nls.localize('SearchSettingsWidget.Placeholder', "Search Settings") + })); this._register(this.searchWidget.onDidChange(value => this.filterPreferences(value.trim()))); this._register(this.searchWidget.onEnter(value => this.preferencesRenderers.focusNextPreference())); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index 1a415662dda..ef461384494 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -16,7 +16,7 @@ import { EditorInput, toResource } from 'vs/workbench/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { Position as EditorPosition, IEditor } from 'vs/platform/editor/common/editor'; +import { IEditor } from 'vs/platform/editor/common/editor'; import { ICommonCodeEditor, IPosition } from 'vs/editor/common/editorCommon'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -30,6 +30,7 @@ import { IPreferencesService, IPreferencesEditorModel, ISetting } from 'vs/workb import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; +import { KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -164,18 +165,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic } openGlobalKeybindingSettings(): TPromise { - const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + '\n[\n]'; - const editableKeybindings = URI.file(this.environmentService.appKeybindingsPath); + return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true }).then(() => null); - // Create as needed and open in editor - return this.createIfNotExists(editableKeybindings, emptyContents).then(() => { - return this.editorService.openEditors([ - { input: { resource: this.defaultKeybindingsResource, options: { pinned: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }, position: EditorPosition.ONE }, - { input: { resource: editableKeybindings, options: { pinned: true } }, position: EditorPosition.TWO }, - ]).then(() => { - this.editorGroupService.focusGroup(EditorPosition.TWO); - }); - }); } configureSettingsForLanguage(language: string): void { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 6e358171a6c..c2125721ffd 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -13,7 +13,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference, IViewZone } from 'vs/editor/browser/editorBrowser'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ISettingsGroup } from 'vs/workbench/parts/preferences/common/preferences'; @@ -238,10 +238,10 @@ export class SearchWidget extends Widget { private _onEnter = this._register(new Emitter()); public onEnter: Event = this._onEnter.event; - constructor(parent: HTMLElement, + constructor(parent: HTMLElement, protected options: IInputOptions, @IContextViewService private contextViewService: IContextViewService, @IContextMenuService private contextMenuService: IContextMenuService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService protected instantiationService: IInstantiationService ) { super(); this.create(parent); @@ -257,14 +257,15 @@ export class SearchWidget extends Widget { private createSearchContainer(searchContainer: HTMLElement) { this.searchContainer = searchContainer; const searchInput = DOM.append(this.searchContainer, DOM.$('div.settings-search-input')); - this.inputBox = this._register(new InputBox(searchInput, this.contextViewService, { - ariaLabel: localize('SearchSettingsWidget.AriaLabel', "Search settings"), - placeholder: localize('SearchSettingsWidget.Placeholder', "Search Settings") - })); + this.inputBox = this.createInputBox(searchInput); this.inputBox.onDidChange(value => this._onDidChange.fire(value)); this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp(e)); } + protected createInputBox(parent: HTMLElement): InputBox { + return this._register(new InputBox(parent, this.contextViewService, this.options)); + } + public showMessage(message: string, count: number): void { this.countElement.textContent = message; this.inputBox.inputElement.setAttribute('aria-label', message); @@ -280,7 +281,6 @@ export class SearchWidget extends Widget { DOM.removeClass(this.countElement, 'hide'); this.inputBox.inputElement.style.paddingRight = DOM.getTotalWidth(this.countElement) + 20 + 'px'; } - } public focus() { diff --git a/src/vs/workbench/parts/preferences/common/keybindingsEditorModel.ts b/src/vs/workbench/parts/preferences/common/keybindingsEditorModel.ts new file mode 100644 index 00000000000..9287fb5e1b3 --- /dev/null +++ b/src/vs/workbench/parts/preferences/common/keybindingsEditorModel.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IMatch, IFilter, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; +import { Registry } from 'vs/platform/platform'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommonEditorRegistry, EditorAction } from 'vs/editor/common/editorCommonExtensions'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; +import { EditorModel } from 'vs/workbench/common/editor'; +import { IExtensionService } from 'vs/platform/extensions/common/extensions'; +import { IKeybindingService, IKeybindingItem2, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; + +export interface IKeybindingItemEntry { + id: string; + keybindingItem: IKeybindingItem; + commandIdMatches?: IMatch[]; + commandLabelMatches?: IMatch[]; + keybindingMatches?: IMatch[]; +} + +export interface IKeybindingItem extends IKeybindingItem2 { + commandLabel: string; +} + +const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString); + +export class KeybindingsEditorModel extends EditorModel { + + private _keybindingItems: IKeybindingItem[]; + + constructor( + @IKeybindingService private keybindingsService: IKeybindingService, + @IExtensionService private extensionService: IExtensionService + ) { + super(); + } + + public fetch(searchValue: string): IKeybindingItemEntry[] { + searchValue = searchValue.trim(); + return searchValue ? this.fetchKeybindingItems(searchValue) : + this._keybindingItems.map(keybindingItem => ({ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem })); + } + + private fetchKeybindingItems(searchValue: string): IKeybindingItemEntry[] { + const result: IKeybindingItemEntry[] = []; + for (const keybindingItem of this._keybindingItems) { + let keybindingMatches: IMatch[] = keybindingItem.keybinding ? KeybindingsEditorModel.matches(searchValue, keybindingItem.keybinding.getAriaLabel(), or(matchesWords, matchesCamelCase)) : null; + let commandIdMatches: IMatch[] = KeybindingsEditorModel.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase)); + let commandLabelMatches: IMatch[] = keybindingItem.commandLabel ? KeybindingsEditorModel.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true)) : null; + if (keybindingMatches || commandIdMatches || commandLabelMatches) { + result.push({ + id: KeybindingsEditorModel.getId(keybindingItem), + commandLabelMatches, + keybindingItem, + keybindingMatches, + commandIdMatches + }); + } + } + return result; + } + + public resolve(): TPromise { + return this.extensionService.onReady() + .then(() => { + const workbenchActionsRegistry = Registry.as(ActionExtensions.WorkbenchActions); + const editorActions = CommonEditorRegistry.getEditorActions().reduce((editorActions, editorAction) => { + editorActions[editorAction.id] = editorAction; + return editorActions; + }, {}); + this._keybindingItems = this.keybindingsService.getKeybindings().map(keybinding => KeybindingsEditorModel.toKeybindingEntry(keybinding, workbenchActionsRegistry, editorActions)); + const boundCommands = this._keybindingItems.reduce((boundCommands, keybinding) => { + boundCommands[keybinding.command] = true; + return boundCommands; + }, {}); + const commandsMap = CommandsRegistry.getCommands(); + for (const command in commandsMap) { + if (!boundCommands[command]) { + this._keybindingItems.push(KeybindingsEditorModel.toKeybindingEntry({ + keybinding: null, + command, + when: null, + source: KeybindingSource.Default + }, workbenchActionsRegistry, editorActions)); + } + } + this._keybindingItems = this._keybindingItems.sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b)); + return this; + }); + } + + private static getId(keybindingItem: IKeybindingItem2): string { + return keybindingItem.command + (keybindingItem.keybinding ? keybindingItem.keybinding.getAriaLabel() : '') + keybindingItem.source + (keybindingItem.when ? keybindingItem.when.serialize() : ''); + } + + private static compareKeybindingData(a: IKeybindingItem, b: IKeybindingItem): number { + if (a.keybinding && !b.keybinding) { + return -1; + } + if (b.keybinding && !a.keybinding) { + return 1; + } + if (a.commandLabel && !b.commandLabel) { + return -1; + } + if (b.commandLabel && !a.commandLabel) { + return 1; + } + if (a.commandLabel && b.commandLabel) { + if (a.commandLabel !== b.commandLabel) { + return a.commandLabel.localeCompare(b.commandLabel); + } + } + if (a.command === b.command) { + return a.source === KeybindingSource.User ? -1 : 1; + } + return a.command.localeCompare(b.command); + } + + private static toKeybindingEntry(keybinding: IKeybindingItem2, workbenchActionsRegistry: IWorkbenchActionRegistry, editorActions: {}): IKeybindingItem { + const workbenchAction = workbenchActionsRegistry.getWorkbenchAction(keybinding.command); + const editorAction: EditorAction = editorActions[keybinding.command]; + return { + keybinding: keybinding.keybinding, + command: keybinding.command, + commandLabel: editorAction ? editorAction.label : workbenchAction ? workbenchAction.label : '', + when: keybinding.when, + source: keybinding.source, + category: editorAction ? localize('editorCategory', "Editor") : workbenchAction ? workbenchActionsRegistry.getCategory(workbenchAction.id) ? workbenchActionsRegistry.getCategory(workbenchAction.id) : null : null + }; + } + + private static matches(searchValue: string, wordToMatchAgainst: string, wordMatchesFilter: IFilter): IMatch[] { + let matches = wordFilter(searchValue, wordToMatchAgainst); + if (!matches) { + const words = searchValue.split(' '); + for (const word of words) { + const wordMatches = wordMatchesFilter(word, wordToMatchAgainst); + if (wordMatches) { + matches = [...(matches || []), ...wordMatches]; + } else { + matches = null; + break; + } + } + } + if (matches) { + matches.sort((a, b) => { + return a.start - b.start; + }); + } + return matches; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 7414e00271f..b83af520712 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -81,4 +81,7 @@ export interface IPreferencesService { } export const CONTEXT_SETTINGS_EDITOR = new RawContextKey('settingsEditor', false); -export const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; \ No newline at end of file +export const CONTEXT_KEYBINDINGS_EDITOR = new RawContextKey('inKeybindingsEditor', false); + +export const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; +export const ACTION_DEFINE_KEYBINDING = 'kebindings.action.define'; \ No newline at end of file diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts new file mode 100644 index 00000000000..50330796f96 --- /dev/null +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import * as strings from 'vs/base/common/strings'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Queue } from 'vs/base/common/async'; +import { IReference, Disposable } from 'vs/base/common/lifecycle'; +import * as json from 'vs/base/common/json'; +import { Edit } from 'vs/base/common/jsonFormatter'; +import { setProperty } from 'vs/base/common/jsonEdit'; +import Event, { Emitter } from 'vs/base/common/event'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IKeybindingItem2, KeybindingSource, IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; + +export const IKeybindingEditingService = createDecorator('keybindingEditingService'); + +export interface IKeybindingEditingService { + + _serviceBrand: ServiceIdentifier; + + editKeybinding(key: string, keybindingItem: IKeybindingItem2): TPromise; + + removeKeybinding(keybindingItem: IKeybindingItem2): TPromise; +} + +export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService { + + public _serviceBrand: any; + private queue: Queue; + + private resource: URI = URI.file(this.environmentService.appKeybindingsPath); + + private _onUpdate = this._register(new Emitter()); + public readonly onUpdate: Event = this._onUpdate.event; + + constructor( + @ITextModelResolverService private textModelResolverService: ITextModelResolverService, + @ITextFileService private textFileService: ITextFileService, + @IFileService private fileService: IFileService, + @IEnvironmentService private environmentService: IEnvironmentService + ) { + super(); + this.queue = new Queue(); + } + + editKeybinding(key: string, keybindingItem: IKeybindingItem2): TPromise { + return this.queue.queue(() => this.doEditKeybinding(key, keybindingItem)); // queue up writes to prevent race conditions + } + + removeKeybinding(keybindingItem: IKeybindingItem2): TPromise { + return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions + } + + private doEditKeybinding(key: string, keybindingItem: IKeybindingItem2): TPromise { + return this.resolveAndValidate() + .then(reference => { + key = new RegExp(/\\/g).test(key) ? key.slice(0, -1) + '\\\\' : key; + const model = reference.object.textEditorModel; + if (keybindingItem.source === KeybindingSource.User) { + this.updateUserKeybinding(key, keybindingItem, model); + } else { + this.updateDefaultKeybinding(key, keybindingItem, model); + } + return this.save().then(() => reference.dispose()); + }); + } + + private doRemoveKeybinding(keybindingItem: IKeybindingItem2): TPromise { + return this.resolveAndValidate() + .then(reference => { + const model = reference.object.textEditorModel; + if (keybindingItem.source === KeybindingSource.User) { + this.removeUserKeybinding(keybindingItem, model); + } else { + this.removeDefaultKeybinding(keybindingItem, model); + } + return this.save().then(() => reference.dispose()); + }); + } + + private save(): TPromise { + return this.textFileService.save(this.resource).then(() => this._onUpdate.fire()); + } + + private updateUserKeybinding(newKey: string, keybindingItem: IKeybindingItem2, model: editorCommon.IModel): void { + const {tabSize, insertSpaces} = model.getOptions(); + const eol = model.getEOL(); + const userKeybindingEntries = json.parse(model.getValue()); + const userKeybindingEntry = this.findUserKeybindingEntry(keybindingItem, userKeybindingEntries); + if (userKeybindingEntry) { + this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntries.indexOf(userKeybindingEntry), 'key'], newKey, { tabSize, insertSpaces, eol })[0], model); + } + } + + private updateDefaultKeybinding(newKey: string, keybindingItem: IKeybindingItem2, model: editorCommon.IModel): void { + const {tabSize, insertSpaces} = model.getOptions(); + const eol = model.getEOL(); + const userKeybindingEntries = json.parse(model.getValue()); + const userKeybindingEntry = this.findUserKeybindingEntry(keybindingItem, userKeybindingEntries); + if (userKeybindingEntry) { + // Update the keybinding with new key + this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntries.indexOf(userKeybindingEntry), 'key'], newKey, { tabSize, insertSpaces, eol })[0], model); + } else { + // Add the new keybinidng with new key + this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, keybindingItem.when, false), { tabSize, insertSpaces, eol })[0], model); + } + if (keybindingItem.keybinding) { + // Unassign the default keybinding + this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.keybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model); + } + } + + private removeUserKeybinding(keybindingItem: IKeybindingItem2, model: editorCommon.IModel): void { + const {tabSize, insertSpaces} = model.getOptions(); + const userKeybindingEntries = json.parse(model.getValue()); + const userKeybindingEntry = this.findUserKeybindingEntry(keybindingItem, userKeybindingEntries); + if (userKeybindingEntry) { + userKeybindingEntries.splice(userKeybindingEntries.indexOf(userKeybindingEntry), 1); + model.setValue(JSON.stringify(userKeybindingEntries, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t')); + } + } + + private removeDefaultKeybinding(keybindingItem: IKeybindingItem2, model: editorCommon.IModel): void { + const {tabSize, insertSpaces} = model.getOptions(); + const eol = model.getEOL(); + this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.keybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model); + } + + private findUserKeybindingEntry(keybindingItem: IKeybindingItem2, userKeybindingEntries: IUserFriendlyKeybinding[]): IUserFriendlyKeybinding { + return userKeybindingEntries.filter(keybinding => { + if (keybinding.command !== keybindingItem.command) { + return false; + } + if (!keybinding.when && !keybindingItem.when) { + return true; + } + if (keybinding.when && keybindingItem.when) { + return ContextKeyExpr.deserialize(keybinding.when).serialize() === keybindingItem.when.serialize(); + } + return false; + })[0]; + } + + private asObject(key: string, command: string, when: ContextKeyExpr, negate: boolean): any { + const object = { key }; + object['command'] = negate ? `-${command}` : command; + if (when) { + object['when'] = when.serialize(); + } + return object; + } + + + private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): void { + const startPosition = model.getPositionAt(edit.offset); + const endPosition = model.getPositionAt(edit.offset + edit.length); + const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); + let currentText = model.getValueInRange(range); + const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); + model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); + } + + + private resolveModelReference(): TPromise> { + return this.fileService.existsFile(this.resource) + .then(exists => { + const result = exists ? TPromise.as(null) : this.fileService.updateContent(this.resource, '{}', { encoding: 'utf8' }); + return result.then(() => this.textModelResolverService.createModelReference(this.resource)); + }); + } + + private resolveAndValidate(): TPromise> { + + // Target cannot be dirty if not writing into buffer + if (this.textFileService.isDirty(this.resource)) { + return TPromise.wrapError(localize('errorKeybindingsFileDirty', "Unable to write because the file is dirty. Please save the **Keybindings** file and try again.")); + } + + return this.resolveModelReference() + .then(reference => { + const model = reference.object.textEditorModel; + if (this.hasParseErrors(model)) { + return TPromise.wrapError(localize('errorInvalidConfiguration', "Unable to write keybindings. Please open **Keybindings file** to correct errors/warnings in the file and try again.")); + } + return reference; + }); + } + + private hasParseErrors(model: editorCommon.IModel): boolean { + const parseErrors: json.ParseError[] = []; + json.parse(model.getValue(), parseErrors, { allowTrailingComma: true }); + return parseErrors.length > 0; + } +} \ No newline at end of file