Skip to content
quarry Artifact [f2a03a986e]
Login

Artifact [f2a03a986e]

Artifact f2a03a986eebcaf833e400ebb3e2646628c020213225892dcc5d94b63eea2e11:


// lines.js — picking lines on the file view.
//
// Every gutter number is a real link to ?ln=N#LN, so with no script a
// click still names a line. With script a click picks without a reload:
// the first click picks a line, a second makes a range from it, a third
// starts again, and shift-click extends whatever is picked. Each pick
// pushes the address, so Back steps through them.
//
// Delegated from the document, like copy.js: the next window of the
// file arrives as a patched fragment and its numbers work unchanged.
(function () {
  "use strict";

  function parse(s) {
    var out = [];
    (s || "").split(/[,+ ]/).forEach(function (part) {
      var m = /^(\d+)(?:-(\d+))?$/.exec(part);
      if (!m) return;
      var a = +m[1], b = m[2] ? +m[2] : a;
      if (a < 1 || b < 1) return;
      out.push(a <= b ? [a, b] : [b, a]);
    });
    out.sort(function (x, y) { return x[0] - y[0]; });
    var merged = [];
    out.forEach(function (r) {
      var last = merged[merged.length - 1];
      if (last && r[0] <= last[1] + 1) last[1] = Math.max(last[1], r[1]);
      else merged.push(r);
    });
    return merged;
  }

  function format(ranges) {
    return ranges.map(function (r) {
      return r[0] === r[1] ? String(r[0]) : r[0] + "-" + r[1];
    }).join(",");
  }

  function label(ranges) {
    var one = ranges.length === 1 && ranges[0][0] === ranges[0][1];
    return (one ? "Line " : "Lines ") + ranges.map(function (r) {
      return r[0] === r[1] ? String(r[0]) : r[0] + "–" + r[1];
    }).join(", ");
  }

  function current() {
    return parse(new URLSearchParams(location.search).get("ln"));
  }

  function paint(ranges) {
    var body = document.getElementById("file-body");
    if (!body) return;
    body.querySelectorAll(".line.hl").forEach(function (el) { el.classList.remove("hl"); });
    ranges.forEach(function (r) {
      for (var n = r[0]; n <= r[1]; n++) {
        var el = document.getElementById("L" + n);
        if (!el) break; // past what is drawn, or past the end
        el.classList.add("hl");
      }
    });
    var bar = document.querySelector("[data-hook=line-bar]");
    if (!bar) return;
    bar.hidden = ranges.length === 0;
    var what = bar.querySelector("[data-hook=line-range]");
    if (what && ranges.length) what.textContent = label(ranges);
    var past = bar.querySelector("[data-hook=line-past]");
    if (past) past.hidden = true; // only the server knows the end
    var link = bar.querySelector("[data-hook=line-link]");
    if (link) link.href = bar.getAttribute("data-base") + "?ln=" + format(ranges);
  }

  function go(ranges) {
    var q = ranges.length ? "?ln=" + format(ranges) : "";
    history.pushState(null, "", location.pathname + q);
    paint(ranges);
  }

  var anchor = 0; // the line a range grows from

  function pick(n, extend) {
    var sel = current();
    if (!anchor && sel.length) anchor = sel[0][0];
    if (extend && anchor) return go([[Math.min(anchor, n), Math.max(anchor, n)]]);
    if (sel.length === 1 && sel[0][0] === sel[0][1] && sel[0][0] !== n) {
      anchor = sel[0][0];
      return go([[Math.min(anchor, n), Math.max(anchor, n)]]);
    }
    anchor = n;
    go([[n, n]]);
  }

  // Copying selects the text first, which always works and is the
  // native copy gesture on a phone, then tries both copy paths: the
  // Clipboard API is missing on a plain-HTTP origin.
  function copy(text, button) {
    var area = document.createElement("textarea");
    area.value = text;
    area.setAttribute("readonly", "");
    area.style.position = "fixed";
    area.style.opacity = "0";
    document.body.appendChild(area);
    area.select();
    var copied = false;
    try { copied = document.execCommand("copy"); } catch (e) { /* refused */ }
    area.remove();
    var done = function () {
      button.classList.add("done");
      setTimeout(function () { button.classList.remove("done"); }, 3000);
    };
    if (navigator.clipboard) {
      navigator.clipboard.writeText(text).then(done, function () { if (copied) done(); });
    } else if (copied) {
      done();
    }
  }

  document.addEventListener("click", function (ev) {
    var t = ev.target.closest && ev.target.closest("#file-body a.ln, [data-hook=line-bar] a, [data-hook=line-bar] button");
    if (!t || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.altKey) return;
    var bar = t.closest("[data-hook=line-bar]");
    if (!bar) {
      ev.preventDefault();
      pick(+t.getAttribute("data-n"), ev.shiftKey);
      return;
    }
    switch (t.getAttribute("data-hook")) {
    case "line-link":
      ev.preventDefault();
      copy(new URL(t.getAttribute("href"), location.href).href, t);
      break;
    case "line-copy":
      var text = [];
      document.querySelectorAll("#file-body .line.hl .cl").forEach(function (el) {
        text.push(el.textContent.replace(/\n$/, ""));
      });
      copy(text.join("\n") + "\n", t);
      break;
    case "line-clear":
      ev.preventDefault();
      anchor = 0;
      go([]);
      break;
    }
  });

  window.addEventListener("popstate", function () { paint(current()); });
})();