///
interface CodeMirrorInstance extends CodeMirror.Editor, CodeMirror.Doc {}
declare namespace CodeMirror {
interface LineHandle {
widgets: unknown[];
lineNo(): number;
}
}
const editor = document.querySelector('.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 (Array.isArray(lineHandle.widgets) && lineHandle.widgets.length > 0) {
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 = `Accept ${branch} Change`): HTMLButtonElement {
const link = document.createElement('button');
link.type = 'button';
link.className = 'btn-link';
link.textContent = title;
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]);
}