提交 b543a60c 编写于 作者: J Joao Moreno

Merge branch 'scm-input-validation-provider'

......@@ -946,6 +946,16 @@
"type": "boolean",
"default": true,
"description": "%config.showInlineOpenFileAction%"
},
"git.inputValidation": {
"type": "string",
"enum": [
"always",
"warn",
"off"
],
"default": "warn",
"description": "%config.inputValidation%"
}
}
},
......
......@@ -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.",
......
......@@ -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<Uri>();
readonly onDidChangeRepository: Event<Uri> = 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;
......
......@@ -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 {
......
......@@ -439,4 +439,50 @@ declare module 'vscode' {
resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<RenameInitialValue>;
}
}
/**
* 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<SourceControlInputBoxValidation | undefined | null>;
}
}
......@@ -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<IInputValidation | undefined> => {
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);
}
}
}
......@@ -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,
......
......@@ -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<string>;
$onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise<void>;
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise<void>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined>;
}
export interface ExtHostTaskShape {
......
......@@ -110,6 +110,10 @@ function compareResourceStates(a: vscode.SourceControlResourceState, b: vscode.S
return result;
}
export interface IValidateInput {
(value: string, cursorPosition: number): vscode.ProviderResult<vscode.SourceControlInputBoxValidation | undefined | null>;
}
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];
}
}
......@@ -1200,6 +1200,12 @@ export enum ColorFormat {
HSL = 2
}
export enum SourceControlInputBoxValidationType {
Error = 0,
Warning = 1,
Information = 2
}
export enum TaskRevealKind {
Always = 1,
......
......@@ -79,12 +79,6 @@ Registry.as<IConfigurationRegistry>(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
......@@ -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<any> => {
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<any>(onKeyUp, onMouseUp)(() => this.inputBox.validate(), null, this.disposables);
anyEvent<any>(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);
......
......@@ -68,6 +68,21 @@ export interface ISCMProvider extends IDisposable {
getOriginalResource(uri: URI): TPromise<URI>;
}
export enum InputValidationType {
Error = 0,
Warning = 1,
Information = 2
}
export interface IInputValidation {
message: string;
type: InputValidationType;
}
export interface IInputValidator {
(value: string, cursorPosition: number): TPromise<IInputValidation | undefined>;
}
export interface ISCMInput {
value: string;
readonly onDidChange: Event<string>;
......@@ -75,7 +90,8 @@ export interface ISCMInput {
placeholder: string;
readonly onDidChangePlaceholder: Event<string>;
lineWarningLength: number | undefined;
validateInput: IInputValidator;
readonly onDidChangeValidateInput: Event<void>;
}
export interface ISCMRepository extends IDisposable {
......
......@@ -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<string>();
get onDidChangePlaceholder(): Event<string> { 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<void>();
get onDidChangeValidateInput(): Event<void> { return this._onDidChangeValidateInput.event; }
}
class SCMRepository implements ISCMRepository {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册