Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/diagnostics.ts
3314 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CodeAction, CodeActionKind, CodeActionProvider, Diagnostic, DiagnosticCollection, DiagnosticSeverity, Disposable, Range, Selection, TextDocument, Uri, WorkspaceEdit, l10n, languages, workspace } from 'vscode';
7
import { mapEvent, filterEvent, dispose } from './util';
8
import { Model } from './model';
9
10
export enum DiagnosticCodes {
11
empty_message = 'empty_message',
12
line_length = 'line_length'
13
}
14
15
export class GitCommitInputBoxDiagnosticsManager {
16
17
private readonly diagnostics: DiagnosticCollection;
18
private readonly severity = DiagnosticSeverity.Warning;
19
private readonly disposables: Disposable[] = [];
20
21
constructor(private readonly model: Model) {
22
this.diagnostics = languages.createDiagnosticCollection();
23
24
this.migrateInputValidationSettings()
25
.then(() => {
26
mapEvent(filterEvent(workspace.onDidChangeTextDocument, e => e.document.uri.scheme === 'vscode-scm'), e => e.document)(this.onDidChangeTextDocument, this, this.disposables);
27
filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.inputValidation') || e.affectsConfiguration('git.inputValidationLength') || e.affectsConfiguration('git.inputValidationSubjectLength'))(this.onDidChangeConfiguration, this, this.disposables);
28
});
29
}
30
31
public getDiagnostics(uri: Uri): ReadonlyArray<Diagnostic> {
32
return this.diagnostics.get(uri) ?? [];
33
}
34
35
private async migrateInputValidationSettings(): Promise<void> {
36
try {
37
const config = workspace.getConfiguration('git');
38
const inputValidation = config.inspect<'always' | 'warn' | 'off' | boolean>('inputValidation');
39
40
if (inputValidation === undefined) {
41
return;
42
}
43
44
// Workspace setting
45
if (typeof inputValidation.workspaceValue === 'string') {
46
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', false);
47
}
48
49
// User setting
50
if (typeof inputValidation.globalValue === 'string') {
51
await config.update('inputValidation', inputValidation.workspaceValue !== 'off', true);
52
}
53
} catch { }
54
}
55
56
private onDidChangeConfiguration(): void {
57
for (const repository of this.model.repositories) {
58
this.onDidChangeTextDocument(repository.inputBox.document);
59
}
60
}
61
62
private onDidChangeTextDocument(document: TextDocument): void {
63
const config = workspace.getConfiguration('git');
64
const inputValidation = config.get<boolean>('inputValidation', false);
65
if (!inputValidation) {
66
this.diagnostics.set(document.uri, undefined);
67
return;
68
}
69
70
if (/^\s+$/.test(document.getText())) {
71
const documentRange = new Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end);
72
const diagnostic = new Diagnostic(documentRange, l10n.t('Current commit message only contains whitespace characters'), this.severity);
73
diagnostic.code = DiagnosticCodes.empty_message;
74
75
this.diagnostics.set(document.uri, [diagnostic]);
76
return;
77
}
78
79
const diagnostics: Diagnostic[] = [];
80
const inputValidationLength = config.get<number>('inputValidationLength', 50);
81
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
82
83
for (let index = 0; index < document.lineCount; index++) {
84
const line = document.lineAt(index);
85
const threshold = index === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
86
87
if (line.text.length > threshold) {
88
const diagnostic = new Diagnostic(line.range, l10n.t('{0} characters over {1} in current line', line.text.length - threshold, threshold), this.severity);
89
diagnostic.code = DiagnosticCodes.line_length;
90
91
diagnostics.push(diagnostic);
92
}
93
}
94
95
this.diagnostics.set(document.uri, diagnostics);
96
}
97
98
dispose() {
99
dispose(this.disposables);
100
}
101
}
102
103
export class GitCommitInputBoxCodeActionsProvider implements CodeActionProvider {
104
105
private readonly disposables: Disposable[] = [];
106
107
constructor(private readonly diagnosticsManager: GitCommitInputBoxDiagnosticsManager) {
108
this.disposables.push(languages.registerCodeActionsProvider({ scheme: 'vscode-scm' }, this));
109
}
110
111
provideCodeActions(document: TextDocument, range: Range | Selection): CodeAction[] {
112
const codeActions: CodeAction[] = [];
113
const diagnostics = this.diagnosticsManager.getDiagnostics(document.uri);
114
const wrapAllLinesCodeAction = this.getWrapAllLinesCodeAction(document, diagnostics);
115
116
for (const diagnostic of diagnostics) {
117
if (!diagnostic.range.contains(range)) {
118
continue;
119
}
120
121
switch (diagnostic.code) {
122
case DiagnosticCodes.empty_message: {
123
const workspaceEdit = new WorkspaceEdit();
124
workspaceEdit.delete(document.uri, diagnostic.range);
125
126
const codeAction = new CodeAction(l10n.t('Clear whitespace characters'), CodeActionKind.QuickFix);
127
codeAction.diagnostics = [diagnostic];
128
codeAction.edit = workspaceEdit;
129
codeActions.push(codeAction);
130
131
break;
132
}
133
case DiagnosticCodes.line_length: {
134
const workspaceEdit = this.getWrapLineWorkspaceEdit(document, diagnostic.range);
135
136
const codeAction = new CodeAction(l10n.t('Hard wrap line'), CodeActionKind.QuickFix);
137
codeAction.diagnostics = [diagnostic];
138
codeAction.edit = workspaceEdit;
139
codeActions.push(codeAction);
140
141
if (wrapAllLinesCodeAction) {
142
wrapAllLinesCodeAction.diagnostics = [diagnostic];
143
codeActions.push(wrapAllLinesCodeAction);
144
}
145
146
break;
147
}
148
}
149
}
150
151
return codeActions;
152
}
153
154
private getWrapLineWorkspaceEdit(document: TextDocument, range: Range): WorkspaceEdit {
155
const lineSegments = this.wrapTextDocumentLine(document, range.start.line);
156
157
const workspaceEdit = new WorkspaceEdit();
158
workspaceEdit.replace(document.uri, range, lineSegments.join('\n'));
159
160
return workspaceEdit;
161
}
162
163
private getWrapAllLinesCodeAction(document: TextDocument, diagnostics: readonly Diagnostic[]): CodeAction | undefined {
164
const lineLengthDiagnostics = diagnostics.filter(d => d.code === DiagnosticCodes.line_length);
165
if (lineLengthDiagnostics.length < 2) {
166
return undefined;
167
}
168
169
const wrapAllLinesCodeAction = new CodeAction(l10n.t('Hard wrap all lines'), CodeActionKind.QuickFix);
170
wrapAllLinesCodeAction.edit = this.getWrapAllLinesWorkspaceEdit(document, lineLengthDiagnostics);
171
172
return wrapAllLinesCodeAction;
173
}
174
175
private getWrapAllLinesWorkspaceEdit(document: TextDocument, diagnostics: Diagnostic[]): WorkspaceEdit {
176
const workspaceEdit = new WorkspaceEdit();
177
178
for (const diagnostic of diagnostics) {
179
const lineSegments = this.wrapTextDocumentLine(document, diagnostic.range.start.line);
180
workspaceEdit.replace(document.uri, diagnostic.range, lineSegments.join('\n'));
181
}
182
183
return workspaceEdit;
184
}
185
186
private wrapTextDocumentLine(document: TextDocument, line: number): string[] {
187
const config = workspace.getConfiguration('git');
188
const inputValidationLength = config.get<number>('inputValidationLength', 50);
189
const inputValidationSubjectLength = config.get<number | undefined>('inputValidationSubjectLength', undefined);
190
const lineLengthThreshold = line === 0 ? inputValidationSubjectLength ?? inputValidationLength : inputValidationLength;
191
192
const lineSegments: string[] = [];
193
const lineText = document.lineAt(line).text.trim();
194
195
let position = 0;
196
while (lineText.length - position > lineLengthThreshold) {
197
const lastSpaceBeforeThreshold = lineText.lastIndexOf(' ', position + lineLengthThreshold);
198
199
if (lastSpaceBeforeThreshold !== -1 && lastSpaceBeforeThreshold > position) {
200
lineSegments.push(lineText.substring(position, lastSpaceBeforeThreshold));
201
position = lastSpaceBeforeThreshold + 1;
202
} else {
203
// Find first space after threshold
204
const firstSpaceAfterThreshold = lineText.indexOf(' ', position + lineLengthThreshold);
205
if (firstSpaceAfterThreshold !== -1) {
206
lineSegments.push(lineText.substring(position, firstSpaceAfterThreshold));
207
position = firstSpaceAfterThreshold + 1;
208
} else {
209
lineSegments.push(lineText.substring(position));
210
position = lineText.length;
211
}
212
}
213
}
214
if (position < lineText.length) {
215
lineSegments.push(lineText.substring(position));
216
}
217
218
return lineSegments;
219
}
220
221
dispose() {
222
dispose(this.disposables);
223
}
224
}
225
226