<commit_before>// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- TODO: Is any other way supported?
/// <reference types="codemirror" />

// eslint-disable-next-line @typescript-eslint/no-namespace -- TODO: Is any other way supported?
declare namespace CodeMirror {
	interface LineHandle {
		widgets: unknown[];
		lineNo: () => number;
	}
}

const editor = document.querySelector<Element & {CodeMirror: CodeMirror.Editor}>('.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: Number.POSITIVE_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, Number.POSITIVE_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]);
}<commit_msg>Avoid conflict with `====` lines in `resolve-conflicts` (#4676)<commit_after>/* eslint-disable unicorn/better-regex -- Go home you're drunk */

import regexJoin from 'regex-join';
import type CodeMirror from 'codemirror';

declare module 'codemirror' {
	interface LineHandle {
		widgets: unknown[];
		lineNo: () => number;
	}
}

const editor = document.querySelector<Element & {CodeMirror: CodeMirror.Editor}>('.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: Number.POSITIVE_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;
}

const currentChange = /^>>>>>>> .+ -- Current Change$/;
const incomingChange = /^<<<<<<< .+ -- Incoming Change$/;
const anyMarker = regexJoin(currentChange, /|/, incomingChange, /|^=======$/);

// Accept one or both of branches and remove unnecessary lines
function acceptBranch(branch: string, line: number): void {
	// This variable is only changed when a marker is encountered and is meant to stay positive/negative until the next marker
	let inDeletableSection = false;

	const linesToRemove: number[] = [];
	editor.eachLine(line, Number.POSITIVE_INFINITY, lineHandle => {
		const currentLine = lineHandle.text;

		// Determine whether to remove the following line(s)
		if (incomingChange.test(currentLine)) {
			inDeletableSection = branch === 'Current';
		} else if (currentLine === '=======') {
			inDeletableSection = branch === 'Incoming';
		}

		// Delete tracked lines and all conflict markers
		if (inDeletableSection || anyMarker.test(currentLine)) {
			linesToRemove.push(lineHandle.lineNo());
		}

		return currentChange.test(currentLine); // `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]);
}

/* eslint-enable unicorn/better-regex */
{'fraction_non_alphanumeric': 0.16016927417286483, 'fraction_numerical': 0.0025647601949217746, 'mean_word_length': 4.5390625, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1179069', 'n_tokens_mistral': 2582, 'n_tokens_neox': 2391, 'n_words': 598}