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.
// Global to hold a reference to the OOUI.WindowManager.
var EasyLinks = {};

// Make sure the modules we need are loaded (will only load if not already)
mw.loader.using(['mediawiki.util', 'oojs-ui-core', 'oojs-ui-windows', 'jquery.ui', 'mediawiki.api'], function () {

  // Wait for the page to be parsed
  $(document).ready(function () { 

    // TODO: Check for special pages and abort or…
    // TODO: …only whitelist namespaces known to make sense.

    // Let's at least disable Special:-pages by default to start.
    if (mw.config.get('wgCanonicalNamespace') === 'Special') {
      return;
    }

    // Create and append a window manager.
    EasyLinks.windowManager = new OO.ui.WindowManager();
    $('body').append(EasyLinks.windowManager.$element);


    // Predefine a dialog that we'll open by name when needed.
    // Specify common defaults here and we'll add the variable stuff when opening.
    EasyLinks.windowManager.addWindows({
      linkDialog: new OO.ui.MessageDialog({
        actions: [
          {
            action: 'accept',
            label: 'Dismiss',
            flags: 'primary'
          }
        ]
      })
    });

    // If URL params include &diff= then this is a diff page, so add diffLink portlet
    if (mw.util.getParamValue('diff')) {
      var diffPortlet = mw.util.addPortletLink(
        'p-cactions', '#', 'Get diff link', 'ca-getdiffwl', 'Get a wikilink to this diff'
      ); 
      $(diffPortlet).click(function (event) {
        event.preventDefault();
        doGetDiffWL();
      });
    }

    // Add permalink portlet unconditionally; if &oldid= is missing we use the
    // latest version of the page.
    var diffPortlet = mw.util.addPortletLink(
      'p-cactions', '#', 'Get permalink', 'ca-getpermalink', 'Get a permanent wikilink to this page'
    ); 
    $(diffPortlet).click(function (event) {
      event.preventDefault();
      doGetPermaWL();
    });

    // If wgAction is "history", add multidiff portlet
    if (mw.config.get('wgAction') === "history") {
      var multidiffPortlet = mw.util.addPortletLink(
        'p-cactions', '#', 'Get multiple diff links', 'ca-getmdiffwl', 'Get wikilinks for multiple diffs'
      ); 
      $(multidiffPortlet).click(function (e) {
        e.preventDefault();
        setupMultiDiff();
      });
    }

    // Add section link and permalink links to all headings (except page title)
    $('h2, h3, h4, h5, h6').has('.mw-headline').each(function() {
      var headline = $(this).children('span.mw-headline');
      var editsection = $(this).children('span.mw-editsection');

      var openbracket = $('<span>[ </span>');
      var closebracket = $('<span> ]</span>');
      var divider = $('<span> | </span>');

      var wlspan = $('<a class="getwikilink" href="#">link</a>').click(function(event) {
        event.preventDefault();
        doGetSectionWL($(headline).attr('id'));
      });
      var permaspan = $('<a class="getpermalink" href="#">permalink</span>').click(function(event) {
        event.preventDefault();
        doGetPermaSectionWL($(headline).attr('id'));
      });
      var containerspan = $('<span class="mw-editsection getlinks"></span>');
      $(containerspan).append(openbracket, wlspan, divider, permaspan, closebracket);
      if ($(editsection).length > 0) {
        $(containerspan).insertAfter(editsection);
      } else {
        $(containerspan).insertAfter(headline);
      }
    });
  }); // END: $(document).ready()

  // Format the timestamp.
  function getTimestring (timestamp) {
    const monthNames = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var t = new Date(timestamp);
    var Y = t.getUTCFullYear();
    var M = monthNames[t.getUTCMonth()];
    var D = t.getUTCDate();
    var h = t.getUTCHours().padStart(2, '0');
    var m = t.getUTCMinutes().padStart(2, '0');
    var s = t.getUTCSeconds();
    return `${h}:${m}, ${D} ${M} ${Y}`;
  }

  // Cobble together the wikitext string for mdiff output.
  function formatMultiDiff(revid, user, revtime, summary) {
    var string = '';
    string += '*<small>{{noping|' + user + '}}@';
    string += '[[Special:Diff/' + revid + '|' + revtime + ']]: </small>';
    string += '<span class="easylinks-editsummary">' + summary + '</span>';
    string += "\n";
    return string;
  }

  // Format the edit summary.
  function getSummary (apicomment) {
    let summary = '';
    if (apicomment === '') {
      summary = '<small><span class="autocomment">[no edit summary provided]</span></small>';
    } else if (/^\/\*.*?\*\/$/.test(apicomment)) {
      summary = apicomment.replace(/^\/\*.*?\*\//, '<span class="autocomment">$&: </span>');
      summary += '<span class="autocomment">[no edit summary provided]</span>';
      summary = '<small>' + summary + '</small>';
    } else {
      summary = apicomment.replace(/^\/\*.*?\*\//, '<span class="autocomment">$&: </span>');
    }
    return summary;
  }

  // Common utility function to put the link(s) on the user's clipboard,
  // and pop up a notification that it happened.
  function copyAndNotify(title, description, data) {
    // Create a textInputWidget to hold the link, and a FieldLayout to wrap it.
    var textInput = new OO.ui.MultilineTextInputWidget({ 
      value: data,
      multiline: true,
      autosize: true,
    });
    var textField = new OO.ui.FieldLayout(textInput, {align: 'top', label: null});

    // Configure the message dialog when it is opened with the window manager's openWindow() method.
    var instance = EasyLinks.windowManager.openWindow('linkDialog', {
      title: title,
      message: textField.$element
    });

    // Select the link in the textInput for easy copying.
    // Has to happen after it's finished opening or the button will steal focus.
    instance.opened.then(function () {

      // Try to close dialog when the user hits Enter (since we steal focus from the button).
      textInput.on('enter', function () {
        EasyLinks.windowManager.getCurrentWindow().close({action: 'accept'});
      });

      // Try to close dialog on copy siince we no longer need it.
      textInput.on('copy', function () {
        EasyLinks.windowManager.getCurrentWindow().close({action: 'accept'});
      });

      // Select the link, copy it to the clipboard, notify the user, and close the dialog.
      textInput.select();
      console.log(window.getSelection().toString());
      if (document.execCommand('copy')) {
        mw.notify(
          description + ' was automatically copied to your clipboard.',
          {title: 'Link copied', type: 'info', tag: 'EasyLinksCopied'}
        );
        EasyLinks.windowManager.getCurrentWindow().close({action: 'accept'});
      } else {
        // Auto-copy failed; don't close the dialog to let the user copy manually.
      }
    });
  } // END: copyAndNotify()

  // Get the diff id from URL params and create a diff wikilink to that rev.
  function doGetDiffWL() {
    var diff = mw.util.getParamValue('diff');
    var diffLink = '[[Special:Diff/' + diff + '|' + diff + ']]';

    copyAndNotify(
      'Internal wikilink to this diff',
      'The wikilink for this diff',
      diffLink
    );
  } // END: doGetDiffWL()

  // Get the old rev id from URL params, if present, or the latest rev otherwise.
  function doGetPermaWL() {
    var oldid;
    let kind = '';
    if (mw.util.getParamValue('oldid')) {
      oldid = mw.util.getParamValue('oldid');
      kind = 'this';
    } else {
      oldid = mw.config.get('wgCurRevisionId');
      kind = 'the latest';
    }
    var oldidLink = '[[Special:PermanentLink/' + oldid + '|' + oldid + ']]';

    copyAndNotify(
      'Internal wikilink to this version',
      'A permanent wikilink to ' + kind + ' revision',
      oldidLink
    );
  } // END: doGetPermaWL()

  // Creare a wikilink to a section, irrespective of namespace.
  function doGetSectionWL(headlineid) {
    var h = $('#' + $.escapeSelector(headlineid));
    var title = $(h).text();
    var page = mw.config.get('wgPageName');

    var sectionLink = '[[' + page + '#' + headlineid + '|' + title + ']]';

    copyAndNotify(
      'Wikilink to this section',
      'A wikilink to this section',
      sectionLink
    );
  } // END: doGetSectionWL()

  // Create a permalink to a specific section.
  function doGetPermaSectionWL(headlineid) {
    var h = $('#' + $.escapeSelector(headlineid));
    var title = $(h).text();
    var oldid;

    // If url contains &oldid= then this is a specific revision of the page,
    // otherwise grab the latest revision and use that.
    let kind = '';
    if (mw.util.getParamValue('oldid')) {
      oldid = mw.util.getParamValue('oldid');
      kind = 'this';
    } else {
      oldid = mw.config.get('wgCurRevisionId');
      kind = 'the latest';
    }

    var permaSectionLink = '[[Special:PermanentLink/' + oldid + '#' + headlineid + '|' + title + ']]';

    copyAndNotify(
      'Permanent wikilink to this section',
      'A wikilink to this section at ' + kind + ' revision',
      permaSectionLink
    );
  } // END: doGetPermaSectionWL()

  // On history pages, let user pick multiple revisions and get a list of diffs
  // for those revisions. Unlike the other functions, this one is "modal": when
  // activated we pop up a dialog to prompt the user to pick revisions, and let
  // them cancel or confirm the selection.
  function setupMultiDiff() {
    $('ul#pagehistory > li').prepend(function() {
      let revid = $(this).data('mwRevid');
      let checkbox = $('<input type="checkbox" />')
        .addClass('mdiff-checkbox')
        .css('margin-left', '1em')
        .css('margin-right', '1em')
        .css('vertical-aliign', 'middle')
        .data('mdiff-revid', revid);
      let cspan = $('<span/>')
        .addClass('mdiff-span')
        .css('background', '#663399')
        .append(checkbox)
        .fadeIn(1000);
      return cspan;
    });

    var dialog = $('<div id="mdiff-dialog"><p>Choose the revisions for which you want diffs.</p></div>');
    dialog.dialog({
      autoOpen: false,
      title: 'Choose revisions',
      position: {my: 'right top', at: 'right top', of: '#mw-content-text'},
      buttons: {
        Cancel: function() {
          $('.mdiff-checkbox').remove();
          $(this).dialog('close');
        },
        Ok: function() {
          $(this).dialog('close');
          var selectedRevs = $('.mdiff-checkbox:checked');
          if (selectedRevs.length === 0) {
            // Nothing selected: remove checkboxes and abort.
            $('.mdiff-checkbox').remove();
            return;
          }
          let revisions = [];
          selectedRevs.each(function() {
            let rev = $(this).data('mdiff-revid');
            revisions.push(rev);
          });

          var diffLinks = '';
          var api = new mw.Api();
          api.get({
            action: 'query',
            prop: 'revisions',
            rvprop: ['ids', 'flags', 'timestamp', 'user', 'size', 'comment', 'tags'],
            revids: revisions
          }).done(function (apiResult) {
            Object.values(apiResult.query.pages).forEach(function(page) {
              page.revisions.forEach(function(p) {
                var revid   = p.revid;
                var user    = p.user;
                var revsize = p.size;
                var revtime = getTimestring(p.timestamp);
                var summary = getSummary(p.comment);
                diffLinks += formatMultiDiff(revid, user, revtime, summary);
              });
            });
            $('.mdiff-checkbox').remove();
            copyAndNotify(
              'Wikilinks for these diffs',
              'A list of wikilinks for these diffs',
              diffLinks
            );
          }); // END: api.get().done()
        } // END: Ok()
      } // END: buttons: {}
    }); // END: dialog.dialog()
    dialog.dialog('open');
  } // END: setupMultiDiff()

}); // END: mw.loader.using()