Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/merge-conflict/src/commandHandler.ts
3296 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
import * as vscode from 'vscode';
6
import * as interfaces from './interfaces';
7
import ContentProvider from './contentProvider';
8
9
interface IDocumentMergeConflictNavigationResults {
10
canNavigate: boolean;
11
conflict?: interfaces.IDocumentMergeConflict;
12
}
13
14
enum NavigationDirection {
15
Forwards,
16
Backwards
17
}
18
19
export default class CommandHandler implements vscode.Disposable {
20
21
private disposables: vscode.Disposable[] = [];
22
private tracker: interfaces.IDocumentMergeConflictTracker;
23
24
constructor(trackerService: interfaces.IDocumentMergeConflictTrackerService) {
25
this.tracker = trackerService.createTracker('commands');
26
}
27
28
begin() {
29
this.disposables.push(
30
this.registerTextEditorCommand('merge-conflict.accept.current', this.acceptCurrent),
31
this.registerTextEditorCommand('merge-conflict.accept.incoming', this.acceptIncoming),
32
this.registerTextEditorCommand('merge-conflict.accept.selection', this.acceptSelection),
33
this.registerTextEditorCommand('merge-conflict.accept.both', this.acceptBoth),
34
this.registerTextEditorCommand('merge-conflict.accept.all-current', this.acceptAllCurrent, this.acceptAllCurrentResources),
35
this.registerTextEditorCommand('merge-conflict.accept.all-incoming', this.acceptAllIncoming, this.acceptAllIncomingResources),
36
this.registerTextEditorCommand('merge-conflict.accept.all-both', this.acceptAllBoth),
37
this.registerTextEditorCommand('merge-conflict.next', this.navigateNext),
38
this.registerTextEditorCommand('merge-conflict.previous', this.navigatePrevious),
39
this.registerTextEditorCommand('merge-conflict.compare', this.compare)
40
);
41
}
42
43
private registerTextEditorCommand(command: string, cb: (editor: vscode.TextEditor, ...args: any[]) => Promise<void>, resourceCB?: (uris: vscode.Uri[]) => Promise<void>) {
44
return vscode.commands.registerCommand(command, (...args) => {
45
if (resourceCB && args.length && args.every(arg => arg && arg.resourceUri)) {
46
return resourceCB.call(this, args.map(arg => arg.resourceUri));
47
}
48
const editor = vscode.window.activeTextEditor;
49
return editor && cb.call(this, editor, ...args);
50
});
51
}
52
53
acceptCurrent(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
54
return this.accept(interfaces.CommitType.Current, editor, ...args);
55
}
56
57
acceptIncoming(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
58
return this.accept(interfaces.CommitType.Incoming, editor, ...args);
59
}
60
61
acceptBoth(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
62
return this.accept(interfaces.CommitType.Both, editor, ...args);
63
}
64
65
acceptAllCurrent(editor: vscode.TextEditor): Promise<void> {
66
return this.acceptAll(interfaces.CommitType.Current, editor);
67
}
68
69
acceptAllIncoming(editor: vscode.TextEditor): Promise<void> {
70
return this.acceptAll(interfaces.CommitType.Incoming, editor);
71
}
72
73
acceptAllCurrentResources(resources: vscode.Uri[]): Promise<void> {
74
return this.acceptAllResources(interfaces.CommitType.Current, resources);
75
}
76
77
acceptAllIncomingResources(resources: vscode.Uri[]): Promise<void> {
78
return this.acceptAllResources(interfaces.CommitType.Incoming, resources);
79
}
80
81
acceptAllBoth(editor: vscode.TextEditor): Promise<void> {
82
return this.acceptAll(interfaces.CommitType.Both, editor);
83
}
84
85
async compare(editor: vscode.TextEditor, conflict: interfaces.IDocumentMergeConflict | null) {
86
87
// No conflict, command executed from command palette
88
if (!conflict) {
89
conflict = await this.findConflictContainingSelection(editor);
90
91
// Still failed to find conflict, warn the user and exit
92
if (!conflict) {
93
vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));
94
return;
95
}
96
}
97
98
const conflicts = await this.tracker.getConflicts(editor.document);
99
100
// Still failed to find conflict, warn the user and exit
101
if (!conflicts) {
102
vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));
103
return;
104
}
105
106
const scheme = editor.document.uri.scheme;
107
let range = conflict.current.content;
108
const leftRanges = conflicts.map(conflict => [conflict.current.content, conflict.range]);
109
const rightRanges = conflicts.map(conflict => [conflict.incoming.content, conflict.range]);
110
111
const leftUri = editor.document.uri.with({
112
scheme: ContentProvider.scheme,
113
query: JSON.stringify({ scheme, range: range, ranges: leftRanges })
114
});
115
116
117
range = conflict.incoming.content;
118
const rightUri = leftUri.with({ query: JSON.stringify({ scheme, ranges: rightRanges }) });
119
120
let mergeConflictLineOffsets = 0;
121
for (const nextconflict of conflicts) {
122
if (nextconflict.range.isEqual(conflict.range)) {
123
break;
124
} else {
125
mergeConflictLineOffsets += (nextconflict.range.end.line - nextconflict.range.start.line) - (nextconflict.incoming.content.end.line - nextconflict.incoming.content.start.line);
126
}
127
}
128
const selection = new vscode.Range(
129
conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character,
130
conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character
131
);
132
133
const docPath = editor.document.uri.path;
134
const fileName = docPath.substring(docPath.lastIndexOf('/') + 1); // avoid NodeJS path to keep browser webpack small
135
const title = vscode.l10n.t("{0}: Current Changes ↔ Incoming Changes", fileName);
136
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
137
const openToTheSide = mergeConflictConfig.get<string>('diffViewPosition');
138
const opts: vscode.TextDocumentShowOptions = {
139
viewColumn: openToTheSide === 'Beside' ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
140
selection
141
};
142
143
if (openToTheSide === 'Below') {
144
await vscode.commands.executeCommand('workbench.action.newGroupBelow');
145
}
146
147
await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, title, opts);
148
}
149
150
navigateNext(editor: vscode.TextEditor): Promise<void> {
151
return this.navigate(editor, NavigationDirection.Forwards);
152
}
153
154
navigatePrevious(editor: vscode.TextEditor): Promise<void> {
155
return this.navigate(editor, NavigationDirection.Backwards);
156
}
157
158
async acceptSelection(editor: vscode.TextEditor): Promise<void> {
159
const conflict = await this.findConflictContainingSelection(editor);
160
161
if (!conflict) {
162
vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));
163
return;
164
}
165
166
let typeToAccept: interfaces.CommitType;
167
let tokenAfterCurrentBlock: vscode.Range = conflict.splitter;
168
169
if (conflict.commonAncestors.length > 0) {
170
tokenAfterCurrentBlock = conflict.commonAncestors[0].header;
171
}
172
173
// Figure out if the cursor is in current or incoming, we do this by seeing if
174
// the active position is before or after the range of the splitter or common
175
// ancestors marker. We can use this trick as the previous check in
176
// findConflictByActiveSelection will ensure it's within the conflict range, so
177
// we don't falsely identify "current" or "incoming" if outside of a conflict range.
178
if (editor.selection.active.isBefore(tokenAfterCurrentBlock.start)) {
179
typeToAccept = interfaces.CommitType.Current;
180
}
181
else if (editor.selection.active.isAfter(conflict.splitter.end)) {
182
typeToAccept = interfaces.CommitType.Incoming;
183
}
184
else if (editor.selection.active.isBefore(conflict.splitter.start)) {
185
vscode.window.showWarningMessage(vscode.l10n.t('Editor cursor is within the common ancestors block, please move it to either the "current" or "incoming" block'));
186
return;
187
}
188
else {
189
vscode.window.showWarningMessage(vscode.l10n.t('Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block'));
190
return;
191
}
192
193
this.tracker.forget(editor.document);
194
conflict.commitEdit(typeToAccept, editor);
195
}
196
197
dispose() {
198
this.disposables.forEach(disposable => disposable.dispose());
199
this.disposables = [];
200
}
201
202
private async navigate(editor: vscode.TextEditor, direction: NavigationDirection): Promise<void> {
203
const navigationResult = await this.findConflictForNavigation(editor, direction);
204
205
if (!navigationResult) {
206
// Check for autoNavigateNextConflict, if it's enabled(which indicating no conflict remain), then do not show warning
207
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
208
if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {
209
return;
210
}
211
vscode.window.showWarningMessage(vscode.l10n.t("No merge conflicts found in this file"));
212
return;
213
}
214
else if (!navigationResult.canNavigate) {
215
vscode.window.showWarningMessage(vscode.l10n.t("No other merge conflicts within this file"));
216
return;
217
}
218
else if (!navigationResult.conflict) {
219
// TODO: Show error message?
220
return;
221
}
222
223
// Move the selection to the first line of the conflict
224
editor.selection = new vscode.Selection(navigationResult.conflict.range.start, navigationResult.conflict.range.start);
225
editor.revealRange(navigationResult.conflict.range, vscode.TextEditorRevealType.Default);
226
}
227
228
private async accept(type: interfaces.CommitType, editor: vscode.TextEditor, ...args: any[]): Promise<void> {
229
230
let conflict: interfaces.IDocumentMergeConflict | null;
231
232
// If launched with known context, take the conflict from that
233
if (args[0] === 'known-conflict') {
234
conflict = args[1];
235
}
236
else {
237
// Attempt to find a conflict that matches the current cursor position
238
conflict = await this.findConflictContainingSelection(editor);
239
}
240
241
if (!conflict) {
242
vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));
243
return;
244
}
245
246
// Tracker can forget as we know we are going to do an edit
247
this.tracker.forget(editor.document);
248
conflict.commitEdit(type, editor);
249
250
// navigate to the next merge conflict
251
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
252
if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {
253
this.navigateNext(editor);
254
}
255
256
}
257
258
private async acceptAll(type: interfaces.CommitType, editor: vscode.TextEditor): Promise<void> {
259
const conflicts = await this.tracker.getConflicts(editor.document);
260
261
if (!conflicts || conflicts.length === 0) {
262
vscode.window.showWarningMessage(vscode.l10n.t("No merge conflicts found in this file"));
263
return;
264
}
265
266
// For get the current state of the document, as we know we are doing to do a large edit
267
this.tracker.forget(editor.document);
268
269
// Apply all changes as one edit
270
await editor.edit((edit) => conflicts.forEach(conflict => {
271
conflict.applyEdit(type, editor.document, edit);
272
}));
273
}
274
275
private async acceptAllResources(type: interfaces.CommitType, resources: vscode.Uri[]): Promise<void> {
276
const documents = await Promise.all(resources.map(resource => vscode.workspace.openTextDocument(resource)));
277
const edit = new vscode.WorkspaceEdit();
278
for (const document of documents) {
279
const conflicts = await this.tracker.getConflicts(document);
280
281
if (!conflicts || conflicts.length === 0) {
282
continue;
283
}
284
285
// For get the current state of the document, as we know we are doing to do a large edit
286
this.tracker.forget(document);
287
288
// Apply all changes as one edit
289
conflicts.forEach(conflict => {
290
conflict.applyEdit(type, document, { replace: (range, newText) => edit.replace(document.uri, range, newText) });
291
});
292
}
293
vscode.workspace.applyEdit(edit);
294
}
295
296
private async findConflictContainingSelection(editor: vscode.TextEditor, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<interfaces.IDocumentMergeConflict | null> {
297
298
if (!conflicts) {
299
conflicts = await this.tracker.getConflicts(editor.document);
300
}
301
302
if (!conflicts || conflicts.length === 0) {
303
return null;
304
}
305
306
for (const conflict of conflicts) {
307
if (conflict.range.contains(editor.selection.active)) {
308
return conflict;
309
}
310
}
311
312
return null;
313
}
314
315
private async findConflictForNavigation(editor: vscode.TextEditor, direction: NavigationDirection, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<IDocumentMergeConflictNavigationResults | null> {
316
if (!conflicts) {
317
conflicts = await this.tracker.getConflicts(editor.document);
318
}
319
320
if (!conflicts || conflicts.length === 0) {
321
return null;
322
}
323
324
const selection = editor.selection.active;
325
if (conflicts.length === 1) {
326
if (conflicts[0].range.contains(selection)) {
327
return {
328
canNavigate: false
329
};
330
}
331
332
return {
333
canNavigate: true,
334
conflict: conflicts[0]
335
};
336
}
337
338
let predicate: (_conflict: any) => boolean;
339
let fallback: () => interfaces.IDocumentMergeConflict;
340
let scanOrder: interfaces.IDocumentMergeConflict[];
341
342
if (direction === NavigationDirection.Forwards) {
343
predicate = (conflict) => selection.isBefore(conflict.range.start);
344
fallback = () => conflicts![0];
345
scanOrder = conflicts;
346
} else if (direction === NavigationDirection.Backwards) {
347
predicate = (conflict) => selection.isAfter(conflict.range.start);
348
fallback = () => conflicts![conflicts!.length - 1];
349
scanOrder = conflicts.slice().reverse();
350
} else {
351
throw new Error(`Unsupported direction ${direction}`);
352
}
353
354
for (const conflict of scanOrder) {
355
if (predicate(conflict) && !conflict.range.contains(selection)) {
356
return {
357
canNavigate: true,
358
conflict: conflict
359
};
360
}
361
}
362
363
// Went all the way to the end, return the head
364
return {
365
canNavigate: true,
366
conflict: fallback()
367
};
368
}
369
}
370
371