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.
// vim: ts=4 sw=4 et ai
( function () {
    var wikitextCache = {};
    function getWikitext( pageName, revision ) {
        if( wikitextCache[ revision ] ) {
            return $.when( wikitextCache[ revision ] );
        }
        return $.getJSON(
            mw.config.get( 'wgScriptPath' ) + '/api.php',
            {
                format: "json",
                action: "query",
                prop: "revisions",
                rvprop: "content",
                rvslots: "main",
                rvlimit: 1,
                rvstartid: revision,
                titles: pageName,
                formatversion: 2,
            }
        ).then( function ( data ) {
            var revObj = data.query.pages[ Object.keys( data.query.pages )[0] ].revisions[0];
            var wikitext = revObj.slots.main.content;
            wikitextCache[ revision ] = wikitext;
            return wikitext;
        } );
    }

    function lineNumFromLineNumCell( cell ) {
        return parseInt( cell.textContent.replace( /,/g, "" ).match( /Line (\d+):/ )[1] );
    }

    function findLineNumberRowIdx( diffTable, direction, rowIdx ) {
        var rows = Array.prototype.slice.call( diffTable.rows );
        var searchRows;
        if( direction === "next" ) {
            searchRows = rows.slice( rowIdx );
        } else {
            searchRows = rows.slice( 0, rowIdx );
            searchRows.reverse();
        }
        return searchRows.find( function ( row ) {
            return row.cells[0].className === "diff-lineno";
        } );
    }

    // I'm probably gonna keep finding off-by-one errors in this code until the heat death of the universe
    function showMore( revision, pageName, direction, rowWithButton, numRowsToShow, targetRow, diffTable ) {
        var rows = Array.prototype.slice.call( diffTable.rows );
        var buttonRowIdx = rows.indexOf( rowWithButton );
        if( direction === "next" ) {
            targetRow = findLineNumberRowIdx( diffTable, "previous", buttonRowIdx );
        }
        var currLineNum = lineNumFromLineNumCell( targetRow.cells[1] );
        var targetRowIdx = Array.prototype.indexOf.call( diffTable.rows, targetRow );
        var currRowIdx = direction === "next" ? buttonRowIdx - 1 : targetRowIdx + 1;
        if( direction === "next" ) {
            currLineNum += buttonRowIdx - rows.indexOf( targetRow ) - 2;
        }
        return getWikitext( pageName, revision ).then( function ( wikitext ) {
            var lines = wikitext.split( "\n" );
            
            var startLineIdx, endLineIdx;
            switch( direction ) {
                case "next":
                    startLineIdx = currLineNum + 1;
                    endLineIdx = currLineNum + numRowsToShow + 1;

                    var nextLineNumberRow = findLineNumberRowIdx( diffTable, "next", buttonRowIdx );
                    if( nextLineNumberRow ) {
                        var limitLineNum = lineNumFromLineNumCell( nextLineNumberRow.cells[1] ) - 1;
                        endLineIdx = Math.min( endLineIdx, limitLineNum );
                    }
                    break;
                case "previous":
                    startLineIdx = currLineNum - numRowsToShow - 1;
                    endLineIdx = currLineNum - 1;

                    var prevLineNumberRow = findLineNumberRowIdx( diffTable, "previous", buttonRowIdx );
                    if( prevLineNumberRow ) {
                        var prevLineNumberRowIdx = rows.indexOf( prevLineNumberRow );
                        var limitLineNum = lineNumFromLineNumCell( prevLineNumberRow.cells[1] ) + ( buttonRowIdx - prevLineNumberRowIdx ) - 2;
                        startLineIdx = Math.max( startLineIdx, limitLineNum );
                    }
                    break;
            }
            startLineIdx = Math.max( 0, startLineIdx );
            endLineIdx = Math.min( lines.length - 1, endLineIdx );
            if( endLineIdx - startLineIdx === 0 ) {
                return true; // no more work can be done
            }
            var actualNumLines = endLineIdx - startLineIdx;
            var linesToShow = lines.slice( startLineIdx, endLineIdx );
            if( direction === "previous" ) {
                linesToShow.reverse();
            }
            var insertionRow = currRowIdx + +( direction === "next" );
            for( var lineCounter = 0; lineCounter < actualNumLines; lineCounter++ ) {
                var currLine = "<div>" + linesToShow[ lineCounter ].replace( /</g, "&lt;" ) + "</div>";
                var newRow = diffTable.insertRow( insertionRow );
                newRow.insertCell( 0 ).appendChild( document.createTextNode( "." ) );
                var newCell = newRow.insertCell( 1 );
                newCell.className = "diff-context";
                newCell.innerHTML = currLine;
                newRow.insertCell( 2 ).appendChild( document.createTextNode( "." ) );
                var newCell2 = newRow.insertCell( 3 );
                newCell2.className = "diff-context";
                newCell2.innerHTML = currLine;
            }
            if( direction === "previous" ) {
                targetRow.cells[0].textContent = "Line " + ( currLineNum - actualNumLines ) + ":";
                targetRow.cells[1].textContent = "Line " + ( currLineNum - actualNumLines ) + ":";
            }
            mw.hook( "diff-update" ).fire( diffTable );
            if( actualNumLines < numRowsToShow ) {
                return true; // we're done
            }
            return false;
        } );
    }

    function makeDropdown() {
        var sel = document.createElement( "select" );
        function add( num ) {
            var opt = document.createElement( "option" );
            opt.value = num;
            opt.textContent = num;
            sel.appendChild( opt );
        }
        add( 1 );
        add( 10 );
        add( 50 );
        add( 100 );
        //add( "all" );
        return sel;
    }

    function addLinks( diffTable ) {
        if( !diffTable.querySelector ) {

            // Assume it's a jquery object
            diffTable = diffTable.get( 0 );
        }

        if( diffTable.getElementsByClassName( "diff-context-container" ).length > 0 ) {

            // We already ran on this diff
            return;
        }

        function makeRow( index, direction, targetRow ) {
            var newRow = diffTable.insertRow( index );
            newRow.className = "context " + direction;
            var newCell = newRow.insertCell( 0 );
            newCell.setAttribute( "colspan", "4" );
            var container = document.createElement( "div" );
            container.className = "diff-context-container";
            container.appendChild( document.createTextNode( "Show " + direction + " " ) );
            var dropdown = makeDropdown();
            container.appendChild( dropdown );
            container.appendChild( document.createTextNode( " lines: " ) );
            var go = document.createElement( "button" );
            go.textContent = "Go";
            var revision = mw.config.get( "wgDiffNewId" );
            var pageName = mw.config.get( "wgPageName" );

            if( diffTable.parentNode && diffTable.parentNode.dataset.mwRevid ) {
                // User contribs page or normal watchlist or history page
                revision = diffTable.parentNode.dataset.mwRevid;
                if( pageName.indexOf( "Special:Contributions" ) === 0 ) {
                    pageName = diffTable.parentNode.querySelector( "a.mw-contributions-title" ).textContent;
                }
            } else if( diffTable.parentNode && diffTable.parentNode.className === "mw-enhanced-rc-nested" ) {
                // Enhanced ("show all changes") watchlist, when multiple revisions are shown for one page
                revision = diffTable.parentNode.parentNode.dataset.mwRevid;
                pageName = diffTable.parentNode.dataset.targetPage;
            } else if( diffTable.id.substring( diffTable.id.length - 7 ) === "display" && diffTable.previousElementSibling.dataset.mwRevid ) {
                // Enhanced watchlist, only one revision shown for a page
                revision = diffTable.previousElementSibling.dataset.mwRevid;
                pageName = diffTable.previousElementSibling.getElementsByClassName( "mw-changeslist-line-inner" )[0].dataset.targetPage;
            }

            go.addEventListener( "click", function ( evt ) {
                var numRowsToShow = parseInt( dropdown.value );
                showMore( revision, pageName, direction, newRow, numRowsToShow, targetRow, diffTable ).then( function ( done ) {
                    if( done ) {
                        this.disabled = true;
                    }
                }.bind( this ) );
                evt.preventDefault();
            }, true );

            container.appendChild( go );
            newCell.appendChild( container );
        }

        var firstTime = true;
        for( var rowIdx = 0, rowCount = diffTable.rows.length; rowIdx < rowCount; rowIdx++ ) {
            var row = diffTable.rows[rowIdx];
            if( row.cells[0].className !== "diff-lineno" ) continue;
            if( !firstTime ) {
                makeRow( rowIdx, "next", row );
                rowIdx++;
            } else {
                firstTime = false;
            }
            makeRow( rowIdx, "previous", row );
            rowIdx++;
        }
        var lineNumCells = diffTable.querySelectorAll( "td.diff-lineno" );
        if( lineNumCells.length ) {
            var lastLineNumRow = Array.prototype.slice.call( lineNumCells, -1 )[0].parentNode;
            makeRow( -1, "next", lastLineNumRow );
        }
    }

    $( function () {
        var diffTable = document.querySelector( "table.diff" );
        if( mw.config.get( "wgDiffNewId" ) && diffTable && diffTable.rows.length > 2 ) {
            mw.loader.addStyleTag( "tr.context td { text-align: center; }" );
            mw.loader.addStyleTag( "tr.context td div.diff-context-container { display: inline-block; border: thin solid #ddd; padding: 0.1em 0.5em; }" );
            addLinks( diffTable );
        }
        mw.hook( "wikipage.diff" ).add( addLinks );
        mw.hook( "new-diff-table" ).add( addLinks );
    } );
} )();