All files / diff/src/format ed.ts

100% Statements 77/77
100% Branches 42/42
100% Functions 6/6
100% Lines 61/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154  7x 7x       2643x                           7x 2702x 2702x 700x 700x 700x 700x 700x         700x 762x 776x 762x 701x 62x       61x                                   7x 743x 743x 2702x 2702x 2643x 2643x 2643x 2643x 1910x 1910x 1910x 1910x 3567x 3567x 2302x 33x 33x   2302x 197x 197x 2105x   1910x                                       7x 7x 7x 7x 7x 7x 7x 7x 7x     7x 5x 5x 5x 10x 10x   5x                         7x 7x                             7x 229x    
import type {LinePatch} from '../line';
import {hunks} from './hunks';
import {expander} from './tabs';
import {type EdOptions, type Hunk, HUNK_OP_TYPE, type TabOptions} from './types';
 
/** Ed range: `4,5` for several lines, `4` for one, and the line an append follows. */
const range = (start: number, count: number): string => (count > 1 ? start + ',' + (start + count - 1) : '' + start);
 
/**
 * Whether a hunk replaces lines with the identical text, which an ed script has
 * no way to express and no reason to. It can only arise from the one difference
 * this format cannot carry: a last line that kept its text and lost or gained
 * its terminator. `lines.diff` never emits a change between equal strings, so
 * nothing else produces this shape.
 *
 * A caller following {@link ed}'s contract - plain arrays, never
 * `format.diffKeys` - cannot produce it either, so this is unreachable from
 * {@link ed} itself. It stays for {@link edHunks}, which takes hunks from
 * anywhere, including a keyed pipeline or a patch read off the wire.
 */
const noop = (hunk: Hunk): boolean => {
  const oldCount = hunk.oldCount;
  if (!oldCount || oldCount !== hunk.newCount) return false;
  const lines = hunk.lines;
  const length = lines.length;
  let i = 0;
  let j = 0;
  let pairs = 0;
  // Walked by side rather than by position: `lines` is not `oldCount` old-side
  // entries followed by the new side unless it came from `hunks()`, and indexing
  // it that way threw on every hunk carrying context and on every one read off
  // the wire, which is exactly what this is documented to take.
  for (;;) {
    while (i < length && lines[i].op === HUNK_OP_TYPE.INS) i++;
    while (j < length && lines[j].op === HUNK_OP_TYPE.DEL) j++;
    if (i === length || j === length) break;
    if (lines[i++].text !== lines[j++].text) return false;
    pairs++;
  }
  // Fewer pairs than the hunk covers means its old side is not in its lines at
  // all - a blind ed delete - and what it replaces is unknown, not identical.
  return pairs === oldCount;
};
 
/**
 * Renders hunks as an `ed` script, `diff -e`, **last hunk first**. An ed script
 * is evaluated as it applies, so a command written top-down would renumber the
 * lines its own later commands address; emitted bottom-up, every address still
 * refers to the file as it was. This is the classic silently-wrong `-e` bug: the
 * output looks plausible and produces the wrong file.
 *
 * Hunks must carry no context (see {@link ed}); each renders as one `Na` / `Nd`
 * / `N,Mc` command.
 *
 * @param hunks Context-free hunks to render, in file order.
 * @param opts The `-t` tab stop, if any. An ed script has no line flag, so a
 *     carriage return re-emits nothing.
 * @returns Chunks of the script, one line each.
 */
export function* edHunks(hunks: Hunk[], opts?: TabOptions): Generator<string> {
  const text = expander(opts?.tabs);
  for (let i = hunks.length - 1; i >= 0; i--) {
    const hunk = hunks[i];
    if (noop(hunk)) continue;
    const oldCount = hunk.oldCount;
    const newCount = hunk.newCount;
    yield range(hunk.oldStart, oldCount) + (oldCount ? (newCount ? 'c' : 'd') : 'a') + '\n';
    if (!newCount) continue;
    const lines = hunk.lines;
    const linesLength = lines.length;
    let inserting = true;
    for (let j = 0; j < linesLength; j++) {
      const line = lines[j];
      if (line.op !== HUNK_OP_TYPE.INS) continue;
      if (!inserting) {
        yield 'a\n';
        inserting = true;
      }
      if (line.text === '.') {
        yield '..\n.\ns/.//\n';
        inserting = false;
      } else yield text(line.text) + '\n';
    }
    if (inserting) yield '.\n';
  }
}
 
/**
 * Renders hunks as a **forward** `ed` script, `diff -f`: the same commands in
 * file order rather than bottom-up, with the letter before the range and the
 * range's two numbers separated by a space.
 *
 * Nothing applies it. Written top-down every command renumbers the lines the
 * ones after it address, so `ed` would produce the wrong file, and the dot
 * escaping {@link edHunks} does is left out - GNU leaves it out too, so a line
 * that is a lone dot ends the insert early and the script does not even parse.
 * It exists because POSIX lists `-f`, and it is a *description* of the changes,
 * not a program.
 *
 * @param hunks Context-free hunks to render, in file order.
 * @param opts The `-t` tab stop, if any.
 * @returns Chunks of the script, one line each.
 */
export function* forwardEdHunks(hunks: Hunk[], opts?: TabOptions): Generator<string> {
  const text = expander(opts?.tabs);
  const length = hunks.length;
  for (let i = 0; i < length; i++) {
    const hunk = hunks[i];
    const oldCount = hunk.oldCount;
    const newCount = hunk.newCount;
    const start = hunk.oldStart;
    yield (oldCount ? (newCount ? 'c' : 'd') : 'a') +
      (oldCount > 1 ? start + ' ' + (start + oldCount - 1) : '' + start) +
      '\n';
    if (!newCount) continue;
    const lines = hunk.lines;
    const linesLength = lines.length;
    for (let j = 0; j < linesLength; j++) {
      const line = lines[j];
      if (line.op === HUNK_OP_TYPE.INS) yield text(line.text) + '\n';
    }
    yield '.\n';
  }
}
 
/**
 * Serializes a line patch as a forward `ed` script, `diff -f`, in chunks.
 *
 * @param src Source lines, without terminators.
 * @param dst Destination lines, without terminators.
 * @param patch A patch between them, from `lines.diff` or `line.diff`.
 * @param opts No-final-newline flags, which the script cannot carry.
 * @returns Chunks of the script, one line each.
 */
export function* forwardEd(src: string[], dst: string[], patch: LinePatch, opts?: EdOptions): Generator<string> {
  yield* forwardEdHunks(hunks(src, dst, patch, {...opts, context: 0}), opts);
}
 
/**
 * Serializes a line patch as an `ed` script, `diff -e`, in chunks. Ed scripts
 * carry no context and no header, so one change run is one command and there is
 * nothing to label.
 *
 * @param src Source lines, without terminators.
 * @param dst Destination lines, without terminators.
 * @param patch A patch between them, from `lines.diff` or `line.diff` over the
 *     **plain** arrays.
 * @param opts No-final-newline flags, which an ed script cannot carry.
 * @returns Chunks of the script, one line each.
 */
export function* ed(src: string[], dst: string[], patch: LinePatch, opts?: EdOptions): Generator<string> {
  yield* edHunks(hunks(src, dst, patch, {...opts, context: 0}), opts);
}