import { CodeAction, CodeActionKind, CodeActionProvider, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, Range, Selection, TextDocument, Uri, WorkspaceEdit, l10n, languages, workspace } from 'vscode';
import { mapEvent, filterEvent, dispose } from './util';
import { Model } from './model';
export enum DiagnosticCodes {
empty_message = 'empty_message',
line_length = 'line_length'
}
export class GitCommitInputBoxDiagnosticsManager {
private readonly diagnostics: DiagnosticCollection;
private readonly severity = DiagnosticSeverity.Warning;
private readonly disposables: Disposable[] = [];
constructor(private readonly model: Model) {
this.diagnostics = languages.createDiagnosticCollection();
this.migrateInputValidationSettings()
.then(() => {
mapEvent(filterEvent(workspace.onDidChangeTextDocument, e => e.document.uri.scheme === 'vscode-scm'), e => e.document)(this.onDidChangeTextDocument, this, this.disposables);
filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.inputValidation') || e.affectsConfiguration('git.inputValidationLength') || e.affectsConfiguration('git.inputValidationSubjectLength'))(this.onDidChangeConfiguration, this, this.disposables);
});
}
public getDiagnostics(uri: Uri): ReadonlyArray<Diagnostic> {
return this.diagnostics.get(uri) ?? [];
}
private async migrateInputValidationSettings(): Promise<void> {
try {
const config = workspace.getConfiguration('git');
const inputValidation = config.inspect<'always' | 'warn' | 'off' | boolean>('inputValidation');
if (inputValidation === undefined) {
return;
}
if (typeof inputValidation.workspaceValue === 'string') {
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', false);
}
if (typeof inputValidation.globalValue === 'string') {
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', true);
}
} catch { }
}
private onDidChangeConfiguration(): void {
for (const repository of this.model.repositories) {
this.onDidChangeTextDocument(repository.inputBox.document);
}
}
private onDidChangeTextDocument(document: TextDocument): void {
const config = workspace.getConfiguration('git');
const inputValidation = config.get<boolean>('inputValidation', false);
if (!inputValidation) {
this.diagnostics.set(document.uri, undefined);
return;
}
if (/^\s+$/.test(document.getText())) {
const documentRange = new Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end);
const diagnostic = new Diagnostic(documentRange, l10n.t('Current commit message only contains whitespace characters'), this.severity);
diagnostic.code = DiagnosticCodes.empty_message;
this.diagnostics.set(document.uri, [diagnostic]);
return;
}
const diagnostics: Diagnostic[] = [];
const inputValidationLength = config.get<number>('inputValidationLength', 50);
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
for (let index = 0; index < document.lineCount; index++) {
const line = document.lineAt(index);
const threshold = index === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
if (line.text.length > threshold) {
const diagnostic = new Diagnostic(line.range, l10n.t('{0} characters over {1} in current line', line.text.length - threshold, threshold), this.severity);
diagnostic.code = DiagnosticCodes.line_length;
diagnostics.push(diagnostic);
}
}
this.diagnostics.set(document.uri, diagnostics);
}
dispose() {
dispose(this.disposables);
}
}
export class GitCommitInputBoxCodeActionsProvider implements CodeActionProvider {
private readonly disposables: Disposable[] = [];
constructor(private readonly diagnosticsManager: GitCommitInputBoxDiagnosticsManager) {
this.disposables.push(languages.registerCodeActionsProvider({ scheme: 'vscode-scm' }, this));
}
provideCodeActions(document: TextDocument, range: Range | Selection): CodeAction[] {
const codeActions: CodeAction[] = [];
const diagnostics = this.diagnosticsManager.getDiagnostics(document.uri);
const wrapAllLinesCodeAction = this.getWrapAllLinesCodeAction(document, diagnostics);
for (const diagnostic of diagnostics) {
if (!diagnostic.range.contains(range)) {
continue;
}
switch (diagnostic.code) {
case DiagnosticCodes.empty_message: {
const workspaceEdit = new WorkspaceEdit();
workspaceEdit.delete(document.uri, diagnostic.range);
const codeAction = new CodeAction(l10n.t('Clear whitespace characters'), CodeActionKind.QuickFix);
codeAction.diagnostics = [diagnostic];
codeAction.edit = workspaceEdit;
codeActions.push(codeAction);
break;
}
case DiagnosticCodes.line_length: {
const workspaceEdit = this.getWrapLineWorkspaceEdit(document, diagnostic.range);
const codeAction = new CodeAction(l10n.t('Hard wrap line'), CodeActionKind.QuickFix);
codeAction.diagnostics = [diagnostic];
codeAction.edit = workspaceEdit;
codeActions.push(codeAction);
if (wrapAllLinesCodeAction) {
wrapAllLinesCodeAction.diagnostics = [diagnostic];
codeActions.push(wrapAllLinesCodeAction);
}
break;
}
}
}
return codeActions;
}
private getWrapLineWorkspaceEdit(document: TextDocument, range: Range): WorkspaceEdit {
const lineSegments = this.wrapTextDocumentLine(document, range.start.line);
const workspaceEdit = new WorkspaceEdit();
workspaceEdit.replace(document.uri, range, lineSegments.join('\n'));
return workspaceEdit;
}
private getWrapAllLinesCodeAction(document: TextDocument, diagnostics: readonly Diagnostic[]): CodeAction | undefined {
const lineLengthDiagnostics = diagnostics.filter(d => d.code === DiagnosticCodes.line_length);
if (lineLengthDiagnostics.length < 2) {
return undefined;
}
const wrapAllLinesCodeAction = new CodeAction(l10n.t('Hard wrap all lines'), CodeActionKind.QuickFix);
wrapAllLinesCodeAction.edit = this.getWrapAllLinesWorkspaceEdit(document, lineLengthDiagnostics);
return wrapAllLinesCodeAction;
}
private getWrapAllLinesWorkspaceEdit(document: TextDocument, diagnostics: Diagnostic[]): WorkspaceEdit {
const workspaceEdit = new WorkspaceEdit();
for (const diagnostic of diagnostics) {
const lineSegments = this.wrapTextDocumentLine(document, diagnostic.range.start.line);
workspaceEdit.replace(document.uri, diagnostic.range, lineSegments.join('\n'));
}
return workspaceEdit;
}
private wrapTextDocumentLine(document: TextDocument, line: number): string[] {
const config = workspace.getConfiguration('git');
const inputValidationLength = config.get<number>('inputValidationLength', 50);
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
const lineLengthThreshold = line === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
const lineSegments: string[] = [];
const lineText = document.lineAt(line).text.trim();
let position = 0;
while (lineText.length - position > lineLengthThreshold) {
const lastSpaceBeforeThreshold = lineText.lastIndexOf(' ', position + lineLengthThreshold);
if (lastSpaceBeforeThreshold !== -1 && lastSpaceBeforeThreshold > position) {
lineSegments.push(lineText.substring(position, lastSpaceBeforeThreshold));
position = lastSpaceBeforeThreshold + 1;
} else {
const firstSpaceAfterThreshold = lineText.indexOf(' ', position + lineLengthThreshold);
if (firstSpaceAfterThreshold !== -1) {
lineSegments.push(lineText.substring(position, firstSpaceAfterThreshold));
position = firstSpaceAfterThreshold + 1;
} else {
lineSegments.push(lineText.substring(position));
position = lineText.length;
}
}
}
if (position < lineText.length) {
lineSegments.push(lineText.substring(position));
}
return lineSegments;
}
dispose() {
dispose(this.disposables);
}
}