// The address of a line, so a reader can send somebody to it. // // In a diff the fragment names the file, the side, and one line or a span of // them: `line-src/pds.js-new:12-20`. The path is written whole, and the side // and the numbers close the fragment, so the reader of a link can see what it // points at. A file's own page has the path in the address already, so there // the fragment is the numbers alone: `L12-20`. /** * The id one diff row answers to, so a link naming a line finds it. * @param {string} path * @param {string} key - the row's key, as `lineKey` writes it * @returns {string} */ export function lineId(path, key) { return `line-${path}-${key}`; } /** * The fragment naming a selected line or span. * @param {{path: string, side: string, start: number, end: number}} selection * @returns {string} */ export function lineFragment(selection) { const at = selection.end === selection.start ? String(selection.start) : `${selection.start}-${selection.end}`; return `line-${selection.path}-${selection.side}:${at}`; } /** A path may hold a hyphen, so the side and the numbers are read from the end. */ const FRAGMENT = /^line-(.+)-(old|new):(\d+)(?:-(\d+))?$/; /** * Read a selection back out of a fragment. * @param {string} hash - the fragment, decoded, without its `#` * @returns {{path: string, side: string, start: number, end: number}|null} */ export function parseLineFragment(hash) { const found = FRAGMENT.exec(hash); if (!found) return null; const [, path, side, first, last] = found; const one = Number(first); const other = last === undefined ? one : Number(last); return { path, side, start: Math.min(one, other), end: Math.max(one, other), }; } /** * The id one source line answers to, which is the fragment naming that line * alone. The browser scrolls to it without being asked. * @param {number} number * @returns {string} */ export function sourceLineId(number) { return `L${number}`; } /** * The fragment naming a line or a span of them on a file's own page. * @param {{start: number, end: number}} span * @returns {string} */ export function sourceFragment(span) { return span.end === span.start ? `L${span.start}` : `L${span.start}-${span.end}`; } /** The second number reads with or without its own L, as a forge writes it. */ const SOURCE = /^L(\d+)(?:-L?(\d+))?$/; /** * Read a span back out of a fragment on a file's own page. * @param {string} hash - the fragment, decoded, without its `#` * @returns {{start: number, end: number}|null} */ export function parseSourceFragment(hash) { const found = SOURCE.exec(hash); if (!found) return null; const one = Number(found[1]); const other = found[2] === undefined ? one : Number(found[2]); return { start: Math.min(one, other), end: Math.max(one, other) }; } /** * The span a click makes. Shift takes it from the line already selected to * the line clicked; anything else selects the one line. * @param {{start: number, end: number}|null|undefined} span * @param {number} at * @param {boolean} [shift] * @returns {{start: number, end: number}} */ export function extendSpan(span, at, shift) { if (shift && span) { return { start: Math.min(span.start, at), end: Math.max(span.start, at) }; } return { start: at, end: at }; } /** * The selection a click makes in a diff. A shift-click reaches the line * already selected only in the same file and on the same side. * @param {{path: string, side: string, start: number, end: number}|null} selection * @param {{path: string, side: string, at: number}} target * @param {boolean} [shift] * @returns {{path: string, side: string, start: number, end: number}} */ export function extendSelection(selection, target, shift) { const { path, side, at } = target; const here = selection?.path === path && selection.side === side ? selection : null; return { path, side, ...extendSpan(here, at, shift) }; } /** * Whether a diff row is one of the lines a selection names. * @param {{path: string, side: string, start: number, end: number}|null|undefined} selection * @param {string} path * @param {{oldLine: number|null, newLine: number|null}} line * @returns {boolean} */ export function inSelection(selection, path, line) { if (!selection || selection.path !== path) return false; const side = line.newLine != null ? 'new' : 'old'; const at = line.newLine ?? line.oldLine; if (at == null || side !== selection.side) return false; return at >= selection.start && at <= selection.end; }