export function computeTextDiff()

in src/widgets/history/diff.ts [36:192]


export function computeTextDiff(before: string, after: string): Diff {
  // Diff the two versions of the text.
  let diff = diffMatchPatch.diff_main(before, after);
  diffMatchPatch.diff_cleanupSemantic(diff);

  // Plaintext for the diff representation.
  let beforeLine = "";
  let afterLine = "";
  let diffLines: DiffLine[] = [];
  let beforeLineChanges: CharacterRange[] = [];
  let afterLineChanges: CharacterRange[] = [];

  function addLines(
    beforeLine: string,
    afterLine: string,
    beforeLineChanges?: CharacterRange[],
    afterLineChanges?: CharacterRange[]
  ): void {
    beforeLineChanges = beforeLineChanges || [];
    afterLineChanges = afterLineChanges || [];
    if (beforeLine === afterLine) {
      diffLines.push({
        text: beforeLine,
        version: "both",
        changeRanges: [],
        index: diffLines.length
      });
    } else {
      if (beforeLine != null) {
        diffLines.push({
          text: beforeLine,
          version: "before",
          changeRanges: beforeLineChanges.concat(),
          index: diffLines.length
        });
        beforeLineChanges = [];
      }
      if (afterLine != null) {
        diffLines.push({
          text: afterLine,
          version: "after",
          changeRanges: afterLineChanges.concat(),
          index: diffLines.length
        });
        afterLineChanges = [];
      }
    }
  }

  // Sort diff segments so that "before" segments always appear before "after" segments.
  // This is so we can make sure to enqueue "before" version of lines before "after" ones.
  diff.sort((segment1, segment2) =>
    segment1[0] === 0 || segment2[0] === 0 ? 0 : segment1[0]
  );

  // Iterate through the list of diff chunks to:
  for (let segment of diff) {
    let action: EditKind = segment[0];
    let substring = segment[1];
    let substringLines = substring.split("\n");

    for (let l = 0; l < substringLines.length; l++) {
      let substringLine = substringLines[l];
      let isLastLine = l === substringLines.length - 1;
      let isInitialNewline = l === 0 && substringLine === "";
      switch (action) {
        case EditKind.Same: // same in both versions
          beforeLine += substringLine;
          afterLine += substringLine;
          if (!isLastLine) {
            addLines(
              beforeLine,
              afterLine,
              beforeLineChanges,
              afterLineChanges
            );
            beforeLine = "";
            afterLine = "";
          }
          break;
        case EditKind.Deletion: // in before, not after
          if (isInitialNewline) substringLine = "⏎";
          beforeLineChanges.push({
            start: beforeLine.length,
            end: beforeLine.length + substringLine.length
          });
          beforeLine += substringLine;
          if (!isLastLine) {
            addLines(beforeLine, null, beforeLineChanges);
            beforeLine = "";
          }
          break;
        case EditKind.Insertion: // in after, not before
          if (isInitialNewline) substringLine = "⏎";
          afterLineChanges.push({
            start: afterLine.length,
            end: afterLine.length + substringLine.length
          });
          afterLine += substringLine;
          if (!isLastLine) {
            addLines(null, afterLine, null, afterLineChanges);
            afterLine = "";
          }
          break;
      }
    }
  }

  // Add any residual before and after lines to the text.
  beforeLine = before.length > 0 ? beforeLine : undefined;
  afterLine = after.length > 0 ? afterLine : undefined;
  addLines(beforeLine, afterLine, beforeLineChanges, afterLineChanges);

  let beforeLineNumbers: number[] = [];
  let afterLineNumbers: number[] = [];
  let changeLocations: Location[] = [];

  // All "before" diff lines should go before "after" diff lines.
  // All other lines should preserve their original order.
  diffLines.sort((diffLine1, diffLine2) => {
    if (
      diffLine1.version === "both" ||
      diffLine2.version === "both" ||
      diffLine1.version === diffLine2.version
    ) {
      return diffLine1.index - diffLine2.index;
    } else return diffLine1.version === "before" ? -1 : 1;
  });

  let diffTextLines = [];
  for (let i = 0; i < diffLines.length; i++) {
    let diffLine = diffLines[i];
    let lineNumber = i + 1;
    diffTextLines.push(diffLine.text);
    if (diffLine.version === "before") {
      beforeLineNumbers.push(lineNumber);
    }
    if (diffLine.version === "after") {
      afterLineNumbers.push(lineNumber);
    }
    for (let range of diffLine.changeRanges) {
      changeLocations.push({
        first_line: lineNumber,
        first_column: range.start,
        last_line: lineNumber,
        last_column: range.end
      });
    }
  }

  return {
    text: diffTextLines.join("\n"),
    beforeLines: beforeLineNumbers,
    afterLines: afterLineNumbers,
    changeLocations: changeLocations
  };
}