From 888f74495d13e4f203696c2f6da5acb557082fb8 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 26 Jan 2018 16:43:53 +0100 Subject: [PATCH] lineWarningLength -> validationProvider --- extensions/git/package.json | 10 +++ extensions/git/package.nls.json | 1 + extensions/git/src/repository.ts | 43 ++++++++++- src/vs/vscode.d.ts | 55 +++++++++++++- .../api/electron-browser/mainThreadSCM.ts | 21 +++++- src/vs/workbench/api/node/extHost.api.impl.ts | 1 + src/vs/workbench/api/node/extHost.protocol.ts | 3 +- src/vs/workbench/api/node/extHostSCM.ts | 35 +++++++-- src/vs/workbench/api/node/extHostTypes.ts | 6 ++ .../scm/electron-browser/scm.contribution.ts | 6 -- .../parts/scm/electron-browser/scmViewlet.ts | 74 +++++++------------ src/vs/workbench/services/scm/common/scm.ts | 18 ++++- .../services/scm/common/scmService.ts | 17 ++++- 13 files changed, 219 insertions(+), 71 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 5834266c26e..fb75e99d69d 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 28c41092842..2056fafa53c 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 cd9c8a197a6..874fa9568c2 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.validationProvider = 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 60976131c02..7abf4f0ce7f 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5986,6 +5986,56 @@ declare module 'vscode' { export function setLanguageConfiguration(language: string, configuration: LanguageConfiguration): Disposable; } + /** + * 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; + } + + /** + * A validation provider which can validate Source Control input. + */ + export interface SourceControlInputBoxValidationProvider { + + /** + * A function that will be called to validate input and give a hint to the user. + * + * @param value The current value of the input box. + * @param cursorPosition The cusror position within the input box. + * @return A human readable string which is presented as diagnostic message. + * Return `undefined`, `null`, or the empty string when 'value' is valid. + */ + validateInput(value: string, cursorPosition: number): ProviderResult; + } + /** * Represents the input box in the Source Control viewlet. */ @@ -6002,9 +6052,10 @@ declare module 'vscode' { placeholder: string; /** - * The warning threshold for lines in the input box. + * A validation provider for the input box. It's possible to change + * the validation provider simply by setting this property to a different value. */ - lineWarningLength: number | undefined; + validationProvider: SourceControlInputBoxValidationProvider; } interface QuickDiffProvider { diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index 31e0348e305..8b8cec71781 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 2336cf37321..c6876a681d1 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -601,6 +601,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 8d51f8782f5..3b4b93e21ef 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; @@ -693,6 +693,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 0487a50e966..a83ec8f00fe 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -140,15 +140,20 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { this._placeholder = placeholder; } - private _lineWarningLength: number | undefined; + private _validationProvider: vscode.SourceControlInputBoxValidationProvider; - get lineWarningLength(): number | undefined { - return this._lineWarningLength; + get validationProvider(): vscode.SourceControlInputBoxValidationProvider { + return this._validationProvider; } - set lineWarningLength(lineWarningLength: number) { - this._proxy.$setLineWarningLength(this._sourceControlHandle, lineWarningLength); - this._lineWarningLength = lineWarningLength; + set validationProvider(provider: vscode.SourceControlInputBoxValidationProvider) { + if (!provider || typeof provider.validateInput !== 'function') { + console.warn('INVALID SCM input box validation provider'); + return; + } + + this._validationProvider = provider; + this._proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, true); } constructor(private _proxy: MainThreadSCMShape, private _sourceControlHandle: number) { @@ -567,4 +572,22 @@ 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); + } + + const result = await sourceControl.inputBox.validationProvider.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 ce9749e81fc..4c6f8f66ac5 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 c8079172f45..b451d5f00d4 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 dfdeb0359ed..aaf0083a392 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 78014460542..a7901d02b82 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 ad66331ddc1..e1ca99a7729 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 { -- GitLab