Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
$.when($.ready, mw.loader.using('jquery.textSelection')).then(function () {
	let section = $('input[name="wpSection"]').val();
	if (section === 'new') return;
	if (section?.startsWith('T-')) {
		section = section.slice(2);
	}
	if (section === '0') {
		section = null;
	}
	mw.loader.addStyleTag('.diffundo-undone{text-decoration:line-through;opacity:0.5}');
	let idxMap = new WeakMap(), offset = 0;
	let handler = function (e) {
		if (e.type === 'keydown' && (
			e.which !== 13 || this !== e.target ||
			e.ctrlKey || e.shiftKey || e.metaKey || e.altKey
		)) {
			return;
		}
		e.preventDefault();
		let $row = $(this);
		let numRow = $row.prevAll().get().find(row => idxMap.has(row));
		let chunkIdx = idxMap.get(numRow);
		if (!chunkIdx && chunkIdx !== 0) {
			mw.notify(`Couldn't get the line number.`, {
				tag: 'diffundo',
				type: 'error'
			});
			return;
		}
		let isUndone = $row.hasClass('diffundo-undone');
		let $toReplace = $row.children(isUndone ? '.diff-deletedline' : '.diff-addedline');
		let $toRestore = $row.children(isUndone ? '.diff-addedline' : '.diff-deletedline');
		let isInsert = !$toReplace.length;
		let isRemove = !$toRestore.length;
		let $midLines = $row.prevUntil(numRow).map(function () {
			return this.querySelector(
				this.classList.contains('diffundo-undone')
					? ':scope > .diff-deletedline'
					: ':scope > .diff-context, :scope > .diff-addedline'
			);
		});
		let lineIdx = chunkIdx + $midLines.length;
		let $textarea = $('#wpTextbox1');
		let lines = $textarea.textSelection('getContents').split('\n');
		let canUndo;
		if (isInsert) {
			canUndo = !$midLines.length ||
				lines[lineIdx - 1] === $midLines[0].textContent;
		} else {
			canUndo = lines[lineIdx] === $toReplace.text();
		}
		if (!canUndo) {
			mw.notify('The line has been modified since the diff.', {
				tag: 'diffundo',
				type: 'warn'
			});
			return;
		}
		let [start, end] = $textarea.textSelection('getCaretPosition', { startAndEnd: true });
		let beforeLen = lines.slice(0, lineIdx).join('').length + lineIdx;
		if (isRemove) {
			let toReplaceLen = lines[lineIdx].length;
			lines.splice(lineIdx, 1);
			[start, end] = [start, end].map(idx => {
				if (idx > beforeLen + toReplaceLen) {
					return idx - toReplaceLen - 1;
				} else if (idx > beforeLen) {
					return beforeLen;
				}
				return idx;
			});
			$row.nextAll().each(function () {
				if (idxMap.has(this)) {
					idxMap.set(this, idxMap.get(this) - 1);
				}
			});
		} else if (isInsert) {
			let text = $toRestore.text();
			lines.splice(lineIdx, 0, text);
			[start, end] = [start, end].map(idx => {
				if (idx > beforeLen) {
					return idx + text.length + 1;
				}
				return idx;
			});
			$row.nextAll().each(function () {
				if (idxMap.has(this)) {
					idxMap.set(this, idxMap.get(this) + 1);
				}
			});
		} else {
			let toReplaceLen = lines[lineIdx].length;
			let text = $toRestore.text();
			lines.splice(lineIdx, 1, text);
			[start, end] = [start, end].map(idx => {
				if (idx > beforeLen + toReplaceLen) {
					return idx - (toReplaceLen - text.length);
				} else if (idx > beforeLen) {
					return beforeLen;
				}
				return idx;
			});
		}
		$textarea.textSelection('setContents', lines.join('\n'));
		$textarea.textSelection('setSelection', { start, end })
			.textSelection('scrollToCaretPosition');
		$row.toggleClass('diffundo-undone', !isUndone);
	};
	mw.hook('wikipage.diff').add(async $diff => {
		let $lineNums = $diff.find('.diff-lineno:last-child');
		if (!$lineNums.length) return;
		if (section) {
			await mw.loader.using('mediawiki.api');
			let response = await new mw.Api().get({
				action: 'parse',
				page: mw.config.get('wgPageName'),
				prop: 'sections|wikitext',
				formatversion: 2
			});
			if (!$diff[0].isConnected) return;
			let charOffset = response.parse.sections.find(s => s.index === section)
				?.byteoffset;
			if (!charOffset && charOffset !== 0) {
				mw.notify(`Couldn't get the section offset.`, {
					tag: 'diffundo',
					type: 'error'
				});
				return;
			}
			offset = charOffset
				? [...response.parse.wikitext].slice(0, charOffset - 1)
					.join('').split('\n').length
				: 0;
		}
		$lineNums.each(function () {
			idxMap.set(this.parentElement, this.textContent.replace(/\D/g, '') - 1 - offset);
		});
		$diff.find('.diff-deletedline, .diff-addedline').parent().attr({
			tabindex: 0,
			role: 'button'
		}).on('dblclick keydown', handler);
	});
});