diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 3c548a008c4b9d1e04543ee8ef5acfdef125b1eb..c942c20d9cc093c032f63dea3b376b29a23b7367 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -11,7 +11,6 @@ import { Model, Resource, Status, CommitOptions } from './model'; import * as staging from './staging'; import * as path from 'path'; import * as os from 'os'; -import { uniqueFilter } from './util'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; @@ -271,9 +270,7 @@ export class CommandCenter { } @command('git.stage') - async stage(...uris: Uri[]): Promise { - const resources = this.toSCMResources(uris); - + async stage(...resources: Resource[]): Promise { if (!resources.length) { return; } @@ -366,9 +363,7 @@ export class CommandCenter { } @command('git.unstage') - async unstage(...uris: Uri[]): Promise { - const resources = this.toSCMResources(uris); - + async unstage(...resources: Resource[]): Promise { if (!resources.length) { return; } @@ -423,9 +418,7 @@ export class CommandCenter { } @command('git.clean') - async clean(...uris: Uri[]): Promise { - const resources = this.toSCMResources(uris); - + async clean(...resources: Resource[]): Promise { if (!resources.length) { return; } @@ -786,12 +779,6 @@ export class CommandCenter { } } - private toSCMResources(uris: Uri[]): Resource[] { - return uris.filter(uniqueFilter(uri => uri.toString())) - .map(uri => this.resolveSCMResource(uri)) - .filter(r => !!r) as Resource[]; - } - dispose(): void { this.disposables.forEach(d => d.dispose()); } diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index fef842230ac8ecc0fa3cb1bbf58e9fae25099fbf..7536ec5210dd1cfaa34d55f43c799c1fcc6d4092 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -30,17 +30,25 @@ function replacer(key: string, value: any): any { return value; } +// TODO@Joao hack? +export const ResolverRegistry: { [mid: number]: (value: any) => any; } = Object.create(null); function reviver(key: string, value: any): any { let marshallingConst: number; if (value !== void 0 && value !== null) { marshallingConst = (value).$mid; } - if (marshallingConst === 1) { - return URI.revive(value); - } else if (marshallingConst === 2) { - return new RegExp(value.source, value.flags); - } else { - return value; + + switch (marshallingConst) { + case 1: return URI.revive(value); + case 2: return new RegExp(value.source, value.flags); + default: + const resolver = ResolverRegistry[marshallingConst]; + + if (resolver) { + return resolver(value); + } else { + return value; + } } } diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index e57ed6493ce0fa7ab3e79bfb9d1b1cbef7b75d19..3773433efc3d43ef92d2b9c82fc4d5d961d303fe 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -92,19 +92,19 @@ const _altKey = new class extends Emitter { } }; -class MenuItemActionItem extends ActionItem { +export class MenuItemActionItem extends ActionItem { private _wantsAltCommand: boolean = false; constructor( action: MenuItemAction, @IKeybindingService private _keybindingService: IKeybindingService, - @IMessageService private _messageService: IMessageService + @IMessageService protected _messageService: IMessageService ) { super(undefined, action, { icon: !!action.class, label: !action.class }); } - private get _commandAction(): IAction { + protected get _commandAction(): IAction { return this._wantsAltCommand && (this._action).alt || this._action; } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 1a2fd7cd76467946e277294c9ac7395962c45a32..2cf5917ab00b869b02c3991e64aed9f6d41f6e76 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -260,6 +260,7 @@ export interface SCMGroupFeatures { } export type SCMRawResource = [ + number /*handle*/, string /*resourceUri*/, modes.Command /*command*/, string[] /*icons: light, dark*/, diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 91cfc7e75182651aacd6deb0ec523cbb2c6552b1..92f855c0f65577600eb0f9a63b8945f53369a59e 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -12,6 +12,7 @@ import { IThreadService } from 'vs/workbench/services/thread/common/threadServic import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { MainContext, MainThreadSCMShape, SCMRawResource } from './extHost.protocol'; import * as vscode from 'vscode'; +import * as marshalling from 'vs/base/common/marshalling'; function getIconPath(decorations: vscode.SourceControlResourceThemableDecorations) { if (!decorations) { @@ -70,6 +71,8 @@ export class ExtHostSCMInputBox { class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceGroup { private static _handlePool: number = 0; + private _resourceHandlePool: number = 0; + private _resourceStates: Map = new Map(); get id(): string { return this._id; @@ -90,16 +93,13 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG this._proxy.$updateGroup(this._sourceControlHandle, this._handle, { hideWhenEmpty }); } - private _resourcesStates: vscode.SourceControlResourceState[] = []; - - get resourceStates(): vscode.SourceControlResourceState[] { - return this._resourcesStates; - } - set resourceStates(resources: vscode.SourceControlResourceState[]) { - this._resourcesStates = resources; + this._resourceStates.clear(); const rawResources = resources.map(r => { + const handle = this._resourceHandlePool++; + this._resourceStates.set(handle, r); + const sourceUri = r.resourceUri.toString(); const command = this._commands.toInternal(r.command); const iconPath = getIconPath(r.decorations); @@ -117,13 +117,16 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG const strikeThrough = r.decorations && !!r.decorations.strikeThrough; - return [sourceUri, command, icons, strikeThrough] as SCMRawResource; + return [handle, sourceUri, command, icons, strikeThrough] as SCMRawResource; }); this._proxy.$updateGroupResourceStates(this._sourceControlHandle, this._handle, rawResources); } - private _handle: number = ExtHostSourceControlResourceGroup._handlePool++; + private _handle: GroupHandle = ExtHostSourceControlResourceGroup._handlePool++; + get handle(): GroupHandle { + return this._handle; + } constructor( private _proxy: MainThreadSCMShape, @@ -135,6 +138,10 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG this._proxy.$registerGroup(_sourceControlHandle, this._handle, _id, _label); } + getResourceState(handle: number): vscode.SourceControlResourceState | undefined { + return this._resourceStates.get(handle); + } + dispose(): void { this._proxy.$unregisterGroup(this._sourceControlHandle, this._handle); } @@ -143,6 +150,7 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG class ExtHostSourceControl implements vscode.SourceControl { private static _handlePool: number = 0; + private _groups: Map = new Map(); get id(): string { return this._id; @@ -186,7 +194,19 @@ class ExtHostSourceControl implements vscode.SourceControl { } createResourceGroup(id: string, label: string): ExtHostSourceControlResourceGroup { - return new ExtHostSourceControlResourceGroup(this._proxy, this._commands, this._handle, id, label); + const group = new ExtHostSourceControlResourceGroup(this._proxy, this._commands, this._handle, id, label); + this._groups.set(group.handle, group); + return group; + } + + getResourceState(groupHandle: GroupHandle, handle: number): vscode.SourceControlResourceState | undefined { + const group = this._groups.get(groupHandle); + + if (!group) { + return undefined; + } + + return group.getResourceState(handle); } dispose(): void { @@ -195,13 +215,15 @@ class ExtHostSourceControl implements vscode.SourceControl { } type ProviderHandle = number; +type GroupHandle = number; +type ResourceStateHandle = number; export class ExtHostSCM { private static _handlePool: number = 0; private _proxy: MainThreadSCMShape; - private _sourceControls: Map = new Map(); + private _sourceControls: Map = new Map(); private _onDidChangeActiveProvider = new Emitter(); get onDidChangeActiveProvider(): Event { return this._onDidChangeActiveProvider.event; } @@ -218,6 +240,17 @@ export class ExtHostSCM { ) { this._proxy = threadService.get(MainContext.MainThreadSCM); this._inputBox = new ExtHostSCMInputBox(this._proxy); + + // TODO@joao HACK + marshalling.ResolverRegistry[3] = value => { + const sourceControl = this._sourceControls.get(value.sourceControlHandle); + + if (!sourceControl) { + return value; + } + + return sourceControl.getResourceState(value.groupHandle, value.handle); + }; } createSourceControl(id: string, label: string): vscode.SourceControl { diff --git a/src/vs/workbench/api/node/mainThreadSCM.ts b/src/vs/workbench/api/node/mainThreadSCM.ts index f3cc5c8547a9272a0fe7355855c1acaa2a905e56..093d695222392348935b4a218484d2187128a74d 100644 --- a/src/vs/workbench/api/node/mainThreadSCM.ts +++ b/src/vs/workbench/api/node/mainThreadSCM.ts @@ -14,8 +14,11 @@ import { ISCMService, ISCMProvider, ISCMResource, ISCMResourceGroup } from 'vs/w import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResource, SCMGroupFeatures } from './extHost.protocol'; +import { Command } from 'vs/editor/common/modes'; interface IMainThreadSCMResourceGroup { + handle: number; + provider: ISCMProvider; uri: URI; features: SCMGroupFeatures; label: string; @@ -23,6 +26,28 @@ interface IMainThreadSCMResourceGroup { resources: ISCMResource[]; } +class MainThreadSCMResource implements ISCMResource { + + constructor( + private sourceControlHandle: number, + private groupHandle: number, + private handle: number, + public sourceUri: URI, + public command: Command, + public resourceGroup: ISCMResourceGroup, + public decorations + ) { } + + toJSON(): any { + return { + $mid: 3, + sourceControlHandle: this.sourceControlHandle, + groupHandle: this.groupHandle, + handle: this.handle + }; + } +} + class MainThreadSCMProvider implements ISCMProvider { private _groups: IMainThreadSCMResourceGroup[] = []; @@ -61,6 +86,8 @@ class MainThreadSCMProvider implements ISCMProvider { $registerGroup(handle: number, id: string, label: string): void { const group: IMainThreadSCMResourceGroup = { + handle, + provider: this, contextKey: id, label, uri: null, @@ -91,7 +118,7 @@ class MainThreadSCMProvider implements ISCMProvider { } group.resources = resources.map(rawResource => { - const [sourceUri, command, icons, strikeThrough] = rawResource; + const [handle, sourceUri, command, icons, strikeThrough] = rawResource; const icon = icons[0]; const iconDark = icons[1] || icon; const decorations = { @@ -100,12 +127,15 @@ class MainThreadSCMProvider implements ISCMProvider { strikeThrough }; - return { - sourceUri: URI.parse(sourceUri), + return new MainThreadSCMResource( + this.handle, + group.handle, + handle, + URI.parse(sourceUri), command, - resourceGroup: group, + group, decorations - }; + ); }); this._onDidChange.fire(); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmMenus.ts b/src/vs/workbench/parts/scm/electron-browser/scmMenus.ts index 342985bd6e3f1c22233738d799578af1953b051f..400209d18f1b1faf78b7a4a9f5256980c0ba3f2f 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmMenus.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmMenus.ts @@ -142,7 +142,7 @@ export class SCMMenus implements IDisposable { const primary = []; const secondary = []; const result = { primary, secondary }; - fillInActions(menu, resource.uri, result, g => g === 'inline'); + fillInActions(menu, null, result, g => g === 'inline'); menu.dispose(); contextKeyService.dispose(); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 5d05d0e93e8081851e37339c5eaf16cb8070ad29..2daea409c23377def7cc194486af05f7cf196622 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -34,16 +34,44 @@ import { IMessageService } from 'vs/platform/message/common/message'; import { IListService } from 'vs/platform/list/browser/listService'; import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions'; -import { createActionItem } from 'vs/platform/actions/browser/menuItemActionItem'; +import { MenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem'; import { SCMMenus } from './scmMenus'; import { ActionBar, IActionItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from "vs/platform/theme/common/themeService"; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IModelService } from 'vs/editor/common/services/modelService'; import { comparePaths } from 'vs/base/common/comparers'; -import URI from 'vs/base/common/uri'; import { isSCMResource } from './scmUtil'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import Severity from 'vs/base/common/severity'; + +// TODO@Joao +// Need to subclass MenuItemActionItem in order to respect +// the action context coming from any action bar, without breaking +// existing users +class SCMMenuItemActionItem extends MenuItemActionItem { + + onClick(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + + this.actionRunner.run(this._commandAction, this._context) + .done(undefined, err => this._messageService.show(Severity.Error, err)); + } +} + +// TODO@Joao +// Rename contextKey to something else +function identityProvider(r: ISCMResourceGroup | ISCMResource): string { + if (isSCMResource(r)) { + const group = r.resourceGroup; + const provider = group.provider; + return `${provider.contextKey}/${group.contextKey}/${r.sourceUri.toString()}`; + } else { + const provider = r.provider; + return `${provider.contextKey}/${r.contextKey}`; + } +} interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -98,22 +126,20 @@ interface ResourceTemplate { class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => URI[]) { + constructor(private getSelectedResources: () => (ISCMResource | ISCMResourceGroup)[]) { super(); } - /** - * Calls the action.run method with the current selection. Note - * that these actions already have the current scm resource context - * within, so we don't want to pass in the selection if there is only - * one selected element. The user should be able to select a single - * item and run an action on another element and only the latter should - * be influenced. - */ - runAction(action: IAction, context?: any): TPromise { + runAction(action: IAction, context: ISCMResource | ISCMResourceGroup): TPromise { if (action instanceof MenuItemAction) { const selection = this.getSelectedResources(); - return selection.length > 1 ? action.run(...selection) : action.run(); + const filteredSelection = selection.filter(s => s !== context); + + if (selection.length === filteredSelection.length || selection.length === 1) { + return action.run(context); + } + + return action.run(context, ...filteredSelection); } return super.runAction(action, context); @@ -128,7 +154,7 @@ class ResourceRenderer implements IRenderer { constructor( private scmMenus: SCMMenus, private actionItemProvider: IActionItemProvider, - private getSelectedResources: () => URI[], + private getSelectedResources: () => ISCMResource[], @IThemeService private themeService: IThemeService, @IInstantiationService private instantiationService: IInstantiationService ) { } @@ -151,6 +177,7 @@ class ResourceRenderer implements IRenderer { renderElement(resource: ISCMResource, index: number, template: ResourceTemplate): void { template.fileLabel.setFile(resource.sourceUri); template.actionBar.clear(); + template.actionBar.context = resource; template.actionBar.push(this.scmMenus.getResourceActions(resource)); toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); @@ -263,7 +290,7 @@ export class SCMViewlet extends Viewlet { ]; this.list = new List(this.listContainer, delegate, renderers, { - identityProvider: e => e.uri.toString(), + identityProvider, keyboardSupport: false }); @@ -337,7 +364,11 @@ export class SCMViewlet extends Viewlet { } getActionItem(action: IAction): IActionItem { - return createActionItem(action, this.keybindingService, this.messageService); + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + return new SCMMenuItemActionItem(action, this.keybindingService, this.messageService); } private onListContextMenu(e: IListContextMenuEvent): void { @@ -353,12 +384,14 @@ export class SCMViewlet extends Viewlet { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => TPromise.as(actions), + getActionsContext: () => element, actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) }); } - private getSelectedResources(): URI[] { - return this.list.getSelectedElements().map(r => r.uri); + private getSelectedResources(): ISCMResource[] { + return this.list.getSelectedElements() + .filter(r => isSCMResource(r)) as ISCMResource[]; } dispose(): void { diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index 3cfcce363834344561db39017eff868fc3a3f6c8..7cf37df304ded2388f50d6e7cf6272e1aff93d21 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -34,6 +34,7 @@ export interface ISCMResource { export interface ISCMResourceGroup { // readonly uri: URI; + readonly provider: ISCMProvider; readonly label: string; readonly contextKey?: string; readonly resources: ISCMResource[];