summaryrefslogtreecommitdiff
path: root/source/resolve-conflicts.ts
blob: 54df2ae49d869cff09e61f1d0528b8e6bae13625 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/// <reference types="codemirror" />

interface CodeMirrorInstance extends CodeMirror.Editor, CodeMirror.Doc {}

declare namespace CodeMirror {
	interface LineHandle {
		widgets: unknown[];
		lineNo(): number;
	}
}

const editor: CodeMirrorInstance = document.querySelector<any>('.CodeMirror').CodeMirror;

// Event fired when each file is loaded
editor.on('swapDoc', () => setTimeout(addWidget, 1));

// Restore widget on undo
editor.on('changes', (_, [firstChange]) => {
	if (firstChange.origin === 'undo' && firstChange.text[0].startsWith('<<<<<<<')) {
		addWidget();

		// Reset cursor position to one line instead of multiple
		editor.setCursor(editor.getCursor());
	}
});

function getLineNumber(lineChild: Element): number {
	return Number(
		lineChild
			.closest('.CodeMirror-gutter-wrapper, .CodeMirror-linewidget')!
			.parentElement!
			.querySelector('.CodeMirror-linenumber')!
			.textContent
	) - 1;
}

function appendLineInfo(lineHandle: CodeMirror.LineHandle, text: string): void {
	// Only append text if it's not already there
	if (!lineHandle.text.includes(text)) {
		const line = lineHandle.lineNo();
		editor.replaceRange(text, {line, ch: Infinity}); // Infinity = end of line
		editor.clearHistory();
	}
}

// Create and add widget if not already in the document
function addWidget(): void {
	editor.eachLine(lineHandle => {
		if (lineHandle.widgets) {
			return;
		}

		if (lineHandle.text.startsWith('<<<<<<<')) {
			appendLineInfo(lineHandle, ' -- Incoming Change');
			const line = lineHandle.lineNo();
			editor.addLineClass(line, '', 'rgh-resolve-conflicts');
			editor.addLineWidget(line, newWidget(), {
				above: true,
				noHScroll: true
			});
		} else if (lineHandle.text.startsWith('>>>>>>>')) {
			appendLineInfo(lineHandle, ' -- Current Change');
		}
	});
}

function createButton(branch: string, title?: string): HTMLButtonElement {
	const link = document.createElement('button');
	link.type = 'button';
	link.className = 'btn-link';
	link.textContent = title || `Accept ${branch} Change`;
	link.addEventListener('click', ({target}) => {
		acceptBranch(branch, getLineNumber(target as Element));
	});
	return link;
}

// Create and return conflict resolve widget for specific conflict
function newWidget(): HTMLDivElement {
	const widget = document.createElement('div');
	widget.style.fontWeight = 'bold';
	widget.append(
		createButton('Current'),
		' | ',
		createButton('Incoming'),
		' | ',
		createButton('Both', 'Accept Both Changes')
	);
	return widget;
}

// Accept one or both of branches and remove unnecessary lines
function acceptBranch(branch: string, line: number): void {
	let deleteNextLine = false;

	const linesToRemove: number[] = [];
	editor.eachLine(line, Infinity, lineHandle => {
		// Determine whether to remove the following line(s)
		if (lineHandle.text.startsWith('<<<<<<<')) {
			deleteNextLine = branch === 'Current';
		} else if (lineHandle.text.startsWith('=======')) {
			deleteNextLine = branch === 'Incoming';
		}

		// Delete tracked lines and all conflict markers
		if (deleteNextLine || /^([<=>])\1{6}/.test(lineHandle.text)) {
			linesToRemove.push(lineHandle.lineNo());
		}

		return lineHandle.text.startsWith('>>>>>>>'); // `true` ends loop
	});

	// Delete all lines at once in a performant way
	const ranges = linesToRemove.map(line => ({
		anchor: {line, ch: 0},
		head: {line, ch: 0}
	}));
	editor.setSelections(ranges);
	editor.execCommand('deleteLine');
	editor.setCursor(linesToRemove[0]);
}