diff --git a/extensions/git/package.json b/extensions/git/package.json index 1611b2c9117c23c3189b16122c4b669ee54981be..286e7ad4d99898b2c68e6c134a1dfe9886115a72 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -946,6 +946,16 @@ "type": "boolean", "default": true, "description": "%config.showInlineOpenFileAction%" + }, + "git.inputValidation": { + "type": "string", + "enum": [ + "always", + "warn", + "off" + ], + "default": "warn", + "description": "%config.inputValidation%" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 28c41092842eb751f50d47bbda77b1ce48ac8dfb..2056fafa53c0c68be6ca79857981ab826764ef1f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -66,6 +66,7 @@ "config.decorations.enabled": "Controls if Git contributes colors and badges to the explorer and the open editors view.", "config.promptToSaveFilesBeforeCommit": "Controls whether Git should check for unsaved files before committing.", "config.showInlineOpenFileAction": "Controls whether to show an inline Open File action in the Git changes view.", + "config.inputValidation": "Controls when to show input validation the input counter.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", "colors.untracked": "Color for untracked resources.", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index cd9c8a197a646c035b3d99e83477ce5e6b89dc20..893b2ce293970561d1cee27b3841b09e4cd71026 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ 'use strict'; -import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento } from 'vscode'; +import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, SourceControlInputBoxValidation, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento, SourceControlInputBoxValidationType } from 'vscode'; import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError, Submodule, DiffOptions } from './git'; import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent } from './util'; import { memoize, throttle, debounce } from './decorators'; @@ -419,6 +419,8 @@ class ProgressManager { export class Repository implements Disposable { + private static readonly InputValidationLength = 72; + private _onDidChangeRepository = new EventEmitter(); readonly onDidChangeRepository: Event = this._onDidChangeRepository.event; @@ -521,7 +523,7 @@ export class Repository implements Disposable { this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message (press {0} to commit)"); this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] }; this._sourceControl.quickDiffProvider = this; - this._sourceControl.inputBox.lineWarningLength = 72; + this._sourceControl.inputBox.validateInput = this.validateInput.bind(this); this.disposables.push(this._sourceControl); this._mergeGroup = this._sourceControl.createResourceGroup('merge', localize('merge changes', "Merge Changes")); @@ -549,6 +551,43 @@ export class Repository implements Disposable { this.status(); } + validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined { + const config = workspace.getConfiguration('git'); + const setting = config.get<'always' | 'warn' | 'off'>('inputValidation'); + + if (setting === 'off') { + return; + } + + let start = 0, end; + let match: RegExpExecArray | null; + const regex = /\r?\n/g; + + while ((match = regex.exec(text)) && position > match.index) { + start = match.index + match[0].length; + } + + end = match ? match.index : text.length; + + const line = text.substring(start, end); + + if (line.length <= Repository.InputValidationLength) { + if (setting !== 'always') { + return; + } + + return { + message: localize('commitMessageCountdown', "{0} characters left in current line", Repository.InputValidationLength - line.length), + type: SourceControlInputBoxValidationType.Information + }; + } else { + return { + message: localize('commitMessageWarning', "{0} characters over {1} in current line", line.length - Repository.InputValidationLength, Repository.InputValidationLength), + type: SourceControlInputBoxValidationType.Warning + }; + } + } + provideOriginalResource(uri: Uri): Uri | undefined { if (uri.scheme !== 'file') { return; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index b6399c188415447acfb6a15cdafb398f16360ca4..8ab0c4c6af71179e3d7a3d5fa7786b0894bc03f0 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -6025,11 +6025,6 @@ declare module 'vscode' { * A string to show as place holder in the input box to guide the user. */ placeholder: string; - - /** - * The warning threshold for lines in the input box. - */ - lineWarningLength: number | undefined; } interface QuickDiffProvider { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index f500fe3282955231a72beebbb3d3090db7711552..654e69c7ab91fecf16a2089c02a1333fad513470 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -439,4 +439,50 @@ declare module 'vscode' { resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } } + + /** + * Represents the validation type of the Source Control input. + */ + export enum SourceControlInputBoxValidationType { + + /** + * Something not allowed by the rules of a language or other means. + */ + Error = 0, + + /** + * Something suspicious but allowed. + */ + Warning = 1, + + /** + * Something to inform about but not a problem. + */ + Information = 2 + } + + export interface SourceControlInputBoxValidation { + + /** + * The validation message to display. + */ + readonly message: string; + + /** + * The validation type. + */ + readonly type: SourceControlInputBoxValidationType; + } + + /** + * Represents the input box in the Source Control viewlet. + */ + export interface SourceControlInputBox { + + /** + * A validation function for the input box. It's possible to change + * the validation provider simply by setting this property to a different function. + */ + validateInput?(value: string, cursorPosition: number): ProviderResult; + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index 31e0348e305f9f5c204f28c06a5d737be0236ce1..8b8cec717818fd3466538dfb20093a461b587cb3 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -10,7 +10,7 @@ import URI from 'vs/base/common/uri'; import Event, { Emitter } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations } from 'vs/workbench/services/scm/common/scm'; +import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation } from 'vs/workbench/services/scm/common/scm'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { Command } from 'vs/editor/common/modes'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -393,13 +393,28 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.placeholder = placeholder; } - $setLineWarningLength(sourceControlHandle: number, lineWarningLength: number): void { + $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void { const repository = this._repositories[sourceControlHandle]; if (!repository) { return; } - repository.input.lineWarningLength = lineWarningLength; + if (enabled) { + repository.input.validateInput = async (value, pos): TPromise => { + const result = await this._proxy.$validateInput(sourceControlHandle, value, pos); + + if (!result) { + return undefined; + } + + return { + message: result[0], + type: result[1] + }; + }; + } else { + repository.input.validateInput = () => TPromise.as(undefined); + } } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index ce084b3fb4b6d2fe7fa2aa04f6f4a1ef8741e64e..78644e0ededde03c01bf4fd17ed88ab0e2a31504 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -602,6 +602,7 @@ export function createApiFactory( StatusBarAlignment: extHostTypes.StatusBarAlignment, SymbolInformation: extHostTypes.SymbolInformation, SymbolKind: extHostTypes.SymbolKind, + SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, TextEditorCursorStyle: TextEditorCursorStyle, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 71ac999ca305f53119f889d1fb845845d6d7cb0b..13bafd044998cff382b92f4fe70d577e87046c1f 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -431,7 +431,7 @@ export interface MainThreadSCMShape extends IDisposable { $setInputBoxValue(sourceControlHandle: number, value: string): void; $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void; - $setLineWarningLength(sourceControlHandle: number, lineWarningLength: number): void; + $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; } export type DebugSessionUUID = string; @@ -694,6 +694,7 @@ export interface ExtHostSCMShape { $provideOriginalResource(sourceControlHandle: number, uri: string): TPromise; $onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise; $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise; + $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined>; } export interface ExtHostTaskShape { diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 0487a50e9663b911e637d3deee0067ff76c6afda..596aa1ead50603686cb475af21e9a99374cd4484 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -110,6 +110,10 @@ function compareResourceStates(a: vscode.SourceControlResourceState, b: vscode.S return result; } +export interface IValidateInput { + (value: string, cursorPosition: number): vscode.ProviderResult; +} + export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { private _value: string = ''; @@ -140,18 +144,31 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { this._placeholder = placeholder; } - private _lineWarningLength: number | undefined; + private _validateInput: IValidateInput; + + get validateInput(): IValidateInput { + if (!this._extension.enableProposedApi) { + throw new Error(`[${this._extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.id}`); + } - get lineWarningLength(): number | undefined { - return this._lineWarningLength; + return this._validateInput; } - set lineWarningLength(lineWarningLength: number) { - this._proxy.$setLineWarningLength(this._sourceControlHandle, lineWarningLength); - this._lineWarningLength = lineWarningLength; + set validateInput(fn: IValidateInput) { + if (!this._extension.enableProposedApi) { + throw new Error(`[${this._extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.id}`); + } + + if (fn && typeof fn !== 'function') { + console.warn('Invalid SCM input box validation function'); + return; + } + + this._validateInput = fn; + this._proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn); } - constructor(private _proxy: MainThreadSCMShape, private _sourceControlHandle: number) { + constructor(private _extension: IExtensionDescription, private _proxy: MainThreadSCMShape, private _sourceControlHandle: number) { // noop } @@ -381,13 +398,14 @@ class ExtHostSourceControl implements vscode.SourceControl { private handle: number = ExtHostSourceControl._handlePool++; constructor( + _extension: IExtensionDescription, private _proxy: MainThreadSCMShape, private _commands: ExtHostCommands, private _id: string, private _label: string, private _rootUri?: vscode.Uri ) { - this._inputBox = new ExtHostSCMInputBox(this._proxy, this.handle); + this._inputBox = new ExtHostSCMInputBox(_extension, this._proxy, this.handle); this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri && _rootUri.toString()); } @@ -503,7 +521,7 @@ export class ExtHostSCM implements ExtHostSCMShape { this.logService.trace('ExtHostSCM#createSourceControl', extension.id, id, label, rootUri); const handle = ExtHostSCM._handlePool++; - const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label, rootUri); + const sourceControl = new ExtHostSourceControl(extension, this._proxy, this._commands, id, label, rootUri); this._sourceControls.set(handle, sourceControl); const sourceControls = this._sourceControlsByExtension.get(extension.id) || []; @@ -567,4 +585,26 @@ export class ExtHostSCM implements ExtHostSCMShape { await group.$executeResourceCommand(handle); } + + async $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined> { + this.logService.trace('ExtHostSCM#$validateInput', sourceControlHandle); + + const sourceControl = this._sourceControls.get(sourceControlHandle); + + if (!sourceControl) { + return TPromise.as(undefined); + } + + if (!sourceControl.inputBox.validateInput) { + return TPromise.as(undefined); + } + + const result = await sourceControl.inputBox.validateInput(value, cursorPosition); + + if (!result) { + return TPromise.as(undefined); + } + + return [result.message, result.type]; + } } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 9a0865262428da000fa4f8b8b7f64912665f2acc..d4afa18100a181712d436f0000495eb703015a5f 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1200,6 +1200,12 @@ export enum ColorFormat { HSL = 2 } +export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2 +} + export enum TaskRevealKind { Always = 1, diff --git a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts index c8079172f457230e6eb8a6f845082fdb4f403269..b451d5f00d46fd347f477f9fd0e89011e0a77720 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts @@ -79,12 +79,6 @@ Registry.as(ConfigurationExtensions.Configuration).regis enum: ['all', 'gutter', 'overview', 'none'], default: 'all', description: localize('diffDecorations', "Controls diff decorations in the editor.") - }, - 'scm.inputCounter': { - type: 'string', - enum: ['always', 'warn', 'off'], - default: 'warn', - description: localize('inputCounter', "Controls when to display the input counter.") } } }); \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 9cfcc4ed51b32f2a6942dd2175d1c05908da540d..d655a4096bf7578ab4ec831933fb97f857e3da6b 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -22,7 +22,7 @@ import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ISCMService, ISCMRepository, ISCMResourceGroup, ISCMResource } from 'vs/workbench/services/scm/common/scm'; +import { ISCMService, ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } from 'vs/workbench/services/scm/common/scm'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -45,7 +45,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IStorageService } from 'vs/platform/storage/common/storage'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; -import { IMessage, InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Command } from 'vs/editor/common/modes'; @@ -57,6 +57,7 @@ import { ISpliceable, ISequence, ISplice } from 'vs/base/common/sequence'; import { firstIndex } from 'vs/base/common/arrays'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ThrottledDelayer } from 'vs/base/common/async'; // TODO@Joao // Need to subclass MenuItemActionItem in order to respect @@ -684,6 +685,14 @@ class ResourceGroupSplicer { } } +function convertValidationType(type: InputValidationType): MessageType { + switch (type) { + case InputValidationType.Information: return MessageType.INFO; + case InputValidationType.Warning: return MessageType.WARNING; + case InputValidationType.Error: return MessageType.ERROR; + } +} + export class RepositoryPanel extends ViewletPanel { private cachedHeight: number | undefined = undefined; @@ -764,62 +773,31 @@ export class RepositoryPanel extends ViewletPanel { this.inputBox.setPlaceHolder(placeholder); }; - const validation = (text: string): IMessage => { - const setting = this.configurationService.getValue<'always' | 'warn' | 'off'>('scm.inputCounter'); - - if (setting === 'off') { - return null; - } - - let position = this.inputBox.inputElement.selectionStart; - let start = 0, end; - let match: RegExpExecArray; - const regex = /\r?\n/g; - - while ((match = regex.exec(text)) && position > match.index) { - start = match.index + match[0].length; - } - - end = match ? match.index : text.length; + const validationDelayer = new ThrottledDelayer(200); - const line = text.substring(start, end); + const validate = () => { + validationDelayer.trigger(async (): TPromise => { + const result = await this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart); - const lineWarningLength = this.repository.input.lineWarningLength; - - if (lineWarningLength === undefined) { - return { - content: localize('commitMessageInfo', "{0} characters in current line", text.length), - type: MessageType.INFO - }; - } - - if (line.length <= lineWarningLength) { - if (setting !== 'always') { - return null; + if (!result) { + this.inputBox.inputElement.removeAttribute('aria-invalid'); + this.inputBox.hideMessage(); + } else { + this.inputBox.inputElement.setAttribute('aria-invalid', 'true'); + this.inputBox.showMessage({ content: result.message, type: convertValidationType(result.type) }); } - - return { - content: localize('commitMessageCountdown', "{0} characters left in current line", lineWarningLength - line.length), - type: MessageType.INFO - }; - } else { - return { - content: localize('commitMessageWarning', "{0} characters over {1} in current line", line.length - lineWarningLength, lineWarningLength), - type: MessageType.WARNING - }; - } + }); }; - this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { - flexibleHeight: true, - validationOptions: { validation: validation } - }); + this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true }); this.disposables.push(attachInputBoxStyler(this.inputBox, this.themeService)); this.disposables.push(this.inputBox); + this.inputBox.onDidChange(validate, null, this.disposables); + const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup'); const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup'); - anyEvent(onKeyUp, onMouseUp)(() => this.inputBox.validate(), null, this.disposables); + anyEvent(onKeyUp, onMouseUp)(() => validate(), null, this.disposables); this.inputBox.value = this.repository.input.value; this.inputBox.onDidChange(value => this.repository.input.value = value, null, this.disposables); diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index 78014460542ebcf28decda44893bd58f07e4d504..a7901d02b82de4c2bcb6366c11562379ac036ebd 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -68,6 +68,21 @@ export interface ISCMProvider extends IDisposable { getOriginalResource(uri: URI): TPromise; } +export enum InputValidationType { + Error = 0, + Warning = 1, + Information = 2 +} + +export interface IInputValidation { + message: string; + type: InputValidationType; +} + +export interface IInputValidator { + (value: string, cursorPosition: number): TPromise; +} + export interface ISCMInput { value: string; readonly onDidChange: Event; @@ -75,7 +90,8 @@ export interface ISCMInput { placeholder: string; readonly onDidChangePlaceholder: Event; - lineWarningLength: number | undefined; + validateInput: IInputValidator; + readonly onDidChangeValidateInput: Event; } export interface ISCMRepository extends IDisposable { diff --git a/src/vs/workbench/services/scm/common/scmService.ts b/src/vs/workbench/services/scm/common/scmService.ts index ad66331ddc13ff3927aa7ceab99389a7224f2399..e1ca99a7729ac6db5d614144301742918bd89661 100644 --- a/src/vs/workbench/services/scm/common/scmService.ts +++ b/src/vs/workbench/services/scm/common/scmService.ts @@ -7,8 +7,9 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; -import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository } from './scm'; +import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; +import { TPromise } from 'vs/base/common/winjs.base'; class SCMInput implements ISCMInput { @@ -40,7 +41,19 @@ class SCMInput implements ISCMInput { private _onDidChangePlaceholder = new Emitter(); get onDidChangePlaceholder(): Event { return this._onDidChangePlaceholder.event; } - public lineWarningLength: number | undefined = undefined; + private _validateInput: IInputValidator = () => TPromise.as(undefined); + + get validateInput(): IInputValidator { + return this._validateInput; + } + + set validateInput(validateInput: IInputValidator) { + this._validateInput = validateInput; + this._onDidChangeValidateInput.fire(); + } + + private _onDidChangeValidateInput = new Emitter(); + get onDidChangeValidateInput(): Event { return this._onDidChangeValidateInput.event; } } class SCMRepository implements ISCMRepository {