UNPKG

@atlaskit/editor-plugin-show-diff

Version:

ShowDiff plugin for @atlaskit/editor-core

1,017 lines • 51 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
import { areDocsEqualByBlockStructureAndText } from '@atlaskit/editor-common/utils/areDocsEqualByBlockStructureAndText';
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
import { Mapping } from '@atlaskit/editor-prosemirror/transform';
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { createAttributionColorMap, getAttributionKey, getAttributionKeyForChange, getColorSchemeForChange, isAttributionColoringEnabled, isContributorTagsEnabled } from '../decorations/colorSchemes/attributions';
import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorationWidgets';
import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
import { extractDiffDescriptors } from '../decorations/decorationKeys';
import { extractContributorTags } from '../decorations/extractContributorTags';
import { createDeletedLineBreakDecoration } from '../decorations/utils/createDeletedLineBreakWidget';
import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
import { createRemovedLozenge } from '../decorations/utils/wrapBlockNodeView';
import { getDefaultDiffType } from '../getDefaultDiffType';
import { getScrollableDecorations } from '../getScrollableDecorations';
import { emptyTextBlockContentPositions } from '../utils/emptyTextBlocks';
import { diffBySteps } from './diffBySteps';
import { groupChangesByBlock } from './groupChangesByBlock';
import { CELL_CONTENT_OFFSET, groupDeletedColumnChanges } from './groupDeletedColumnChanges';
import { isMarkOnlyChange } from './isMarkOnlyChange';
import { isOpenTokenOnlyChange } from './isOpenTokenOnlyChange';
import { optimizeChanges } from './optimizeChanges';
import { selectTokenEncoder } from './selectTokenEncoder';
import { collapseOverlappingChanges, simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
import { simplifySteps, simplifyStepsWithAttribution } from './simplifySteps';
import { classifySmartChanges } from './smart/classifySmartChanges';
import { smartChangeLevel } from './smart/helpers';
var getChanges = function getChanges(_ref) {
  var changeset = _ref.changeset,
    originalDoc = _ref.originalDoc,
    steppedDoc = _ref.steppedDoc,
    diffType = _ref.diffType,
    tr = _ref.tr,
    steps = _ref.steps,
    intl = _ref.intl,
    smartThresholds = _ref.smartThresholds;
  // The `smart` diff type is gated behind `platform_editor_ai_smart_diff`. When the gate is
  // off, `smart` falls through to the default (`inline`) path below so behaviour degrades
  // gracefully (see docs/smart-diff-design.md §3).
  if (diffType === 'smart' && fg('platform_editor_ai_smart_diff')) {
    var _changes = simplifyChanges(changeset.changes, tr.doc);
    return classifySmartChanges({
      changes: _changes,
      originalDoc: originalDoc,
      newDoc: tr.doc,
      locale: intl.locale,
      thresholds: smartThresholds
    });
  }
  if (diffType === 'step') {
    return diffBySteps(originalDoc, steps);
  }
  if (diffType === 'block') {
    return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc);
  }
  var changes = simplifyChanges(changeset.changes, tr.doc);
  return optimizeChanges(changes);
};
var getAttributedChanges = function getAttributedChanges(_ref2) {
  var attributedChanges = _ref2.attributedChanges,
    changeset = _ref2.changeset,
    originalDoc = _ref2.originalDoc,
    steppedDoc = _ref2.steppedDoc,
    diffType = _ref2.diffType,
    tr = _ref2.tr;
  // `smart` is deliberately absent here, even though `getChanges` applies it: the classifier drops
  // the step attribution and promotes ranges to sentence/paragraph/node granularity, so a promoted
  // range covers several actors' steps and the latest-writer rule credits all of it to one of them.
  // The attributed changeset keeps step granularity instead.
  if (diffType === 'step') {
    return attributedChanges;
  }
  if (diffType === 'block') {
    return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc);
  }
  return simplifyChangesWithAttribution(attributedChanges, tr.doc);
};

/**
 * Collect the inline-content ranges of every leaf text-bearing block (paragraph, heading, …)
 * whose content overlaps `[from, to)`. Used to clip a node-level `smart` insertion highlight to
 * the actual added text, so the inserted background/underline never spans structural gaps
 * (list markers, empty item slots, cell/column boundaries) which would render as phantom rows.
 */
var leafTextblockRanges = function leafTextblockRanges(doc, from, to) {
  var ranges = [];
  doc.nodesBetween(from, to, function (node, pos) {
    if (node.isTextblock && node.content.size > 0) {
      var contentFrom = Math.max(pos + 1, from);
      var contentTo = Math.min(pos + 1 + node.content.size, to);
      if (contentTo > contentFrom) {
        ranges.push([contentFrom, contentTo]);
      }
      // Textblocks have no block children to descend into.
      return false;
    }
    return true;
  });
  return ranges;
};

// A large inserted table is treated with a coarse decoration path (see below)
// once it has more leaf textblocks (~cells) than this threshold. Small tables
// keep the precise per-cell path.
var LARGE_TABLE_LEAF_THRESHOLD = 40;
/**
 * An active scrollable change may contain several underlying diff changes. With attribution, only
 * ranges belonging to the selected contributor (including the anonymous contributor) are active.
 */
var isRangeActive = function isRangeActive(activeIndexPos, from, to, attributionKey) {
  if (activeIndexPos === undefined) {
    return undefined;
  }
  if (!fg('confluence_ncs_step_diffing_version_history')) {
    return from === activeIndexPos.from && to === activeIndexPos.to;
  }
  return from <= activeIndexPos.to && activeIndexPos.from <= to && (!('attributionKey' in activeIndexPos) || activeIndexPos.attributionKey === attributionKey);
};
var isLargeInsertedTableRange = function isLargeInsertedTableRange(doc, from, to) {
  var containsTable = false;
  var leafCount = 0;
  doc.nodesBetween(from, to, function (node) {
    if (node.type.name === 'table') {
      containsTable = true;
    }
    if (node.isTextblock) {
      leafCount += 1;
      return false;
    }
    return true;
  });
  return containsTable && leafCount > LARGE_TABLE_LEAF_THRESHOLD;
};

// Collect the [from, to) ranges of every `table` node overlapping `[from, to)`. The coarse
// large-table path must only suppress decorations inside these ranges
var tableRanges = function tableRanges(doc, from, to) {
  var ranges = [];
  doc.nodesBetween(from, to, function (node, pos) {
    if (node.type.name === 'table') {
      ranges.push([pos, pos + node.nodeSize]);
      return false;
    }
    return true;
  });
  return ranges;
};
var isInsideTable = function isInsideTable(tables, pos) {
  return tables.some(function (_ref3) {
    var _ref4 = _slicedToArray(_ref3, 2),
      tFrom = _ref4[0],
      tTo = _ref4[1];
    return pos >= tFrom && pos < tTo;
  });
};

/**
 * Where a block's content ends, excluding the run of close tokens that ends it: the block's own,
 * then its last child's, and so on down to the last textblock. A change that keeps those trailing
 * close tokens unchanged (because the other side ends in the same structure, e.g.
 * `…</p></li></ul>`) still covers all of the block's content.
 */
var endBeforeTrailingCloseTokens = function endBeforeTrailingCloseTokens(node, pos) {
  var end = pos + node.nodeSize;
  var current = node;
  while (current && !current.isLeaf) {
    end -= 1;
    if (current.isTextblock) {
      break;
    }
    current = current.lastChild;
  }
  return end;
};
var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration(_ref5) {
  var _ref5$allowTrailingCl = _ref5.allowTrailingCloseTokens,
    allowTrailingCloseTokens = _ref5$allowTrailingCl === void 0 ? false : _ref5$allowTrailingCl,
    attributionKey = _ref5.attributionKey,
    doc = _ref5.doc,
    from = _ref5.from,
    to = _ref5.to,
    colorScheme = _ref5.colorScheme,
    _ref5$isInserted = _ref5.isInserted,
    isInserted = _ref5$isInserted === void 0 ? true : _ref5$isInserted,
    leftAnchorId = _ref5.leftAnchorId,
    activeIndexPos = _ref5.activeIndexPos,
    _ref5$shouldHideDelet = _ref5.shouldHideDeleted,
    shouldHideDeleted = _ref5$shouldHideDelet === void 0 ? false : _ref5$shouldHideDelet,
    _ref5$showContributor = _ref5.showContributorTags,
    showContributorTags = _ref5$showContributor === void 0 ? false : _ref5$showContributor,
    _ref5$showIndicators = _ref5.showIndicators,
    showIndicators = _ref5$showIndicators === void 0 ? false : _ref5$showIndicators,
    _ref5$coarseTableCell = _ref5.coarseTableCellsOnly,
    coarseTableCellsOnly = _ref5$coarseTableCell === void 0 ? false : _ref5$coarseTableCell,
    tagMountContext = _ref5.tagMountContext,
    nodeRanges = _ref5.nodeRanges;
  var decorations = [];

  // Exact-nodes path: only reached for a split change (EDITOR-8932), which names the nodes it
  // owns. Deliberately kept separate from the walk below rather than sharing it.
  if (nodeRanges) {
    var _iterator = _createForOfIteratorHelper(nodeRanges),
      _step;
    try {
      for (_iterator.s(); !(_step = _iterator.n()).done;) {
        var _step$value = _step.value,
          pos = _step$value.from,
          nodeEnd = _step$value.to;
        var node = doc.nodeAt(pos);
        if (!(node !== null && node !== void 0 && node.isBlock)) {
          continue;
        }
        var isActive = isRangeActive(activeIndexPos, pos, nodeEnd, attributionKey);
        decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
          attributionKey: attributionKey,
          change: {
            from: pos,
            to: nodeEnd,
            name: node.type.name
          },
          colorScheme: colorScheme,
          isInserted: isInserted,
          leftAnchorId: leftAnchorId,
          isActive: isActive,
          shouldHideDeleted: shouldHideDeleted,
          showContributorTags: showContributorTags,
          showIndicators: showIndicators,
          doc: doc,
          tagMountContext: tagMountContext
        })));
      }
    } catch (err) {
      _iterator.e(err);
    } finally {
      _iterator.f();
    }
    return decorations;
  }

  // Coarse path: inside the large table, only cell/header overlays are kept — the
  // table/row/paragraph decorations there are redundant and cause expensive per-cell
  // re-render. Blocks outside the table keep their normal decorations.
  var coarseTables = coarseTableCellsOnly ? tableRanges(doc, from, to) : [];
  // nodesBetween visits ancestors that start before the changed range.
  // Only fully contained blocks should mark existing list bullets as new. Under patch 2 this also
  // keeps the clean view from hiding a block the change only starts inside: that block holds
  // unchanged text and the deleted-content widget, so `display: none` on it hid the whole diff.
  var requireContainedBlocks = UNSAFE_expValNoExposure('platform_editor_ai_review_moment', 'isEnabled', false) === true || fg('platform_editor_ai_show_diff_patch_2');
  // Under patch 2, a block whose content the change covers but whose trailing close tokens it does
  // not (e.g. the last list item when both sides end in a list) is decorated like its siblings.
  // Otherwise the clean view hides its text but leaves the block, e.g. an empty bullet.
  var relaxTrailingCloseTokens = allowTrailingCloseTokens && fg('platform_editor_ai_show_diff_patch_2');
  var isContained = function isContained(node, pos) {
    if (requireContainedBlocks && pos < from) {
      return false;
    }
    if (pos + node.nodeSize <= to) {
      return true;
    }
    return relaxTrailingCloseTokens && pos >= from && endBeforeTrailingCloseTokens(node, pos) <= to;
  };
  // Iterate over the document nodes within the range
  doc.nodesBetween(from, to, function (node, pos) {
    if (coarseTableCellsOnly && isInsideTable(coarseTables, pos)) {
      var name = node.type.name;
      if (name !== 'tableCell' && name !== 'tableHeader') {
        return;
      }
    }
    if (node.isBlock && isContained(node, pos)) {
      var _nodeEnd = pos + node.nodeSize;
      var _isActive = isRangeActive(activeIndexPos, pos, _nodeEnd, attributionKey);
      decorations.push.apply(decorations, _toConsumableArray(createBlockChangedDecoration({
        attributionKey: attributionKey,
        change: {
          from: pos,
          to: _nodeEnd,
          name: node.type.name
        },
        colorScheme: colorScheme,
        isInserted: isInserted,
        leftAnchorId: leftAnchorId,
        isActive: _isActive,
        shouldHideDeleted: shouldHideDeleted,
        showContributorTags: showContributorTags,
        showIndicators: showIndicators,
        doc: doc,
        tagMountContext: tagMountContext
      })));
    }
  });
  return decorations;
};
/**
 * Whether deleted content for `change` renders after the new content instead of before it.
 *
 * show-diff splits this across two options by change granularity, so the level decides which one
 * applies:
 * - node/paragraph-level: `deletedDiffPlacement` (default `'top'`). Pure deletions are promoted to
 *   node level, so they are governed here too.
 * - inline/sentence-level: `inlineDeletedDiffPlacement` (default `'before'`).
 *
 * Callers are responsible for the `smart` diffType and gate checks.
 */
export var isDeletedContentPlacedBelow = function isDeletedContentPlacedBelow(_ref6) {
  var change = _ref6.change,
    deletedDiffPlacement = _ref6.deletedDiffPlacement,
    inlineDeletedDiffPlacement = _ref6.inlineDeletedDiffPlacement;
  var level = smartChangeLevel(change);
  if (level === 'node' || level === 'paragraph') {
    return deletedDiffPlacement === 'bottom';
  }

  // Inline-level (undefined) and sentence-level changes.
  return inlineDeletedDiffPlacement === 'after';
};

/**
 * Single place `platform_editor_diff_hide_pure_deletions` is read, shared by the widget-anchor
 * calculation and the decoration push below so the two cannot drift.
 *
 * The clean view ("new state") historically only suppressed a deleted side when the same change
 * also carried an insertion. A PURE deletion — text removed with nothing put in its place — fell
 * through and leaked struck-through text into a view that is supposed to show none. `smart` never
 * showed this because it pairs a rewrite into a single change that always carries an insertion;
 * `inline` emits delete-only changes, so it did.
 *
 * With the gate off this returns exactly what both callers computed before it existed.
 *
 * Umbrella experiment: within the AI streaming UX M1 population this behaviour is only enabled
 * for the treatment cohort. Non-AI callers (track-changes, etc.) are not enrolled and default
 * to `true`, preserving the pre-umbrella baseline.
 */
var shouldHideDeletedSide = function shouldHideDeletedSide(_ref7) {
  var change = _ref7.change,
    hideDeletedDiffs = _ref7.hideDeletedDiffs,
    isInverted = _ref7.isInverted;
  return !isInverted && !!hideDeletedDiffs && (change.inserted.length > 0 || fg('platform_editor_diff_hide_pure_deletions') && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true);
};
var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref8) {
  var state = _ref8.state,
    pluginState = _ref8.pluginState,
    nodeViewSerializer = _ref8.nodeViewSerializer,
    colorScheme = _ref8.colorScheme,
    intl = _ref8.intl,
    activeIndexPos = _ref8.activeIndexPos,
    api = _ref8.api,
    _ref8$isInverted = _ref8.isInverted,
    isInverted = _ref8$isInverted === void 0 ? false : _ref8$isInverted,
    _ref8$diffType = _ref8.diffType,
    diffType = _ref8$diffType === void 0 ? getDefaultDiffType() : _ref8$diffType,
    _ref8$hideDeletedDiff = _ref8.hideDeletedDiffs,
    hideDeletedDiffs = _ref8$hideDeletedDiff === void 0 ? false : _ref8$hideDeletedDiff,
    _ref8$hideAddedDiffsU = _ref8.hideAddedDiffsUnderline,
    hideAddedDiffsUnderlineParam = _ref8$hideAddedDiffsU === void 0 ? false : _ref8$hideAddedDiffsU,
    _ref8$showIndicators = _ref8.showIndicators,
    showIndicators = _ref8$showIndicators === void 0 ? false : _ref8$showIndicators,
    smartThresholds = _ref8.smartThresholds,
    _ref8$deletedDiffPlac = _ref8.deletedDiffPlacement,
    deletedDiffPlacement = _ref8$deletedDiffPlac === void 0 ? 'top' : _ref8$deletedDiffPlac,
    _ref8$inlineDeletedDi = _ref8.inlineDeletedDiffPlacement,
    inlineDeletedDiffPlacement = _ref8$inlineDeletedDi === void 0 ? 'before' : _ref8$inlineDeletedDi,
    reveal = _ref8.reveal,
    tagMountContext = _ref8.tagMountContext;
  var originalDoc = pluginState.originalDoc,
    steps = pluginState.steps,
    stepAttributions = pluginState.stepAttributions,
    contributors = pluginState.contributors,
    isDisplayingChanges = pluginState.isDisplayingChanges;
  if (!originalDoc || !isDisplayingChanges) {
    return {
      contributorTags: [],
      decorations: DecorationSet.empty,
      diffDescriptors: []
    };
  }

  // Resolve the option against its gate once here, so every downstream inline/block builder
  // receives the same value. When the gate is off the option is a no-op.
  var hideAddedDiffsUnderline = hideAddedDiffsUnderlineParam && fg('platform_editor_ai_smart_diff');
  var tr = state.tr;
  var steppedDoc = originalDoc;
  var attributionColoringEnabled = isAttributionColoringEnabled(stepAttributions);
  var showContributorTags = isContributorTagsEnabled(stepAttributions, contributors);
  // Tags need the attributed changeset even on diffs with too few actors to be worth colouring.
  var attributedChangesetEnabled = attributionColoringEnabled || showContributorTags;
  var simplifiedSteps;
  var simplifiedStepAttributions = [];
  if (attributedChangesetEnabled) {
    var simplifiedStepsWithAttribution = simplifyStepsWithAttribution(steps, stepAttributions !== null && stepAttributions !== void 0 ? stepAttributions : [], originalDoc);
    simplifiedSteps = simplifiedStepsWithAttribution.map(function (_ref9) {
      var step = _ref9.step;
      return step;
    });
    simplifiedStepAttributions = simplifiedStepsWithAttribution.map(function (_ref0) {
      var stepAttribution = _ref0.stepAttribution;
      return stepAttribution;
    });
  } else {
    simplifiedSteps = simplifySteps(steps, originalDoc);
  }
  var pendingAttrSteps = [];
  var stepMaps = [];
  var _iterator2 = _createForOfIteratorHelper(simplifiedSteps.entries()),
    _step2;
  try {
    for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
      var _step2$value = _slicedToArray(_step2.value, 2),
        stepIndex = _step2$value[0],
        step = _step2$value[1];
      var result = step.apply(steppedDoc);
      if (result.failed === null && result.doc) {
        if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
          pendingAttrSteps.push({
            attributionKey: getAttributionKey(simplifiedStepAttributions[stepIndex]),
            afterNode: result.doc.nodeAt(step.pos),
            beforeNode: steppedDoc.nodeAt(step.pos),
            mapIndex: stepMaps.length,
            step: step
          });
        }
        stepMaps.push(step.getMap());
        steppedDoc = result.doc;
      }
    }
    // Mapped positions and step-time nodes are used together so an attr change is attributed to the
    // node it actually happened on, even when later steps move it.
  } catch (err) {
    _iterator2.e(err);
  } finally {
    _iterator2.f();
  }
  var attrStepContexts = pendingAttrSteps.flatMap(function (_ref1) {
    var afterNode = _ref1.afterNode,
      attributionKey = _ref1.attributionKey,
      beforeNode = _ref1.beforeNode,
      mapIndex = _ref1.mapIndex,
      step = _ref1.step;
    var finalPosition = new Mapping(stepMaps.slice(mapIndex + 1)).mapResult(step.pos);
    if (finalPosition.deleted) {
      return [];
    }
    var originalPosition = new Mapping(stepMaps.slice(0, mapIndex)).invert().mapResult(step.pos);
    return [_objectSpread(_objectSpread({
      attributionKey: attributionKey,
      afterNode: afterNode,
      beforeNode: beforeNode,
      finalPos: finalPosition.pos
    }, originalPosition.deleted ? {} : {
      originalPos: originalPosition.pos
    }), {}, {
      step: step
    })];
  });

  // Rather than using .eq() we use a custom function that only checks for structural
  // changes and ignores differences in attributes which don't affect decoration positions
  if (!areNodesEqualIgnoreAttrs(steppedDoc, tr.doc)) {
    var _api$analytics;
    var recoveredViaContentEquality = areDocsEqualByBlockStructureAndText(steppedDoc, tr.doc);
    api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
      eventType: 'track',
      action: 'nodesNotEqual',
      actionSubject: 'showDiff',
      attributes: {
        docSizeEqual: steppedDoc.nodeSize === tr.doc.nodeSize,
        colorScheme: colorScheme,
        recoveredViaContentEquality: recoveredViaContentEquality
      }
    });
    if (!recoveredViaContentEquality) {
      return {
        contributorTags: [],
        decorations: DecorationSet.empty,
        diffDescriptors: []
      };
    }
  }
  // The attribute-aware encoder is needed by the smart classifier and is gated with it.
  // The NCS version-history path also uses it so the main diff calculation detects attribute-only
  // node replacements, e.g. changing an emoji or mention without changing the node type.
  var _selectTokenEncoder = selectTokenEncoder(diffType === 'smart' && fg('platform_editor_ai_smart_diff') || fg('confluence_ncs_step_diffing_version_history')),
    tokenEncoder = _selectTokenEncoder.tokenEncoder,
    shouldHideMarkOnlyDeletions = _selectTokenEncoder.shouldHideMarkOnlyDeletions;
  var changes;
  var contentChanges;
  var attributedChanges = [];
  var attributionColors;
  if (attributedChangesetEnabled) {
    var changeset = ChangeSet.create(originalDoc, function (left, right) {
      return getAttributionKey(left) === getAttributionKey(right) ? left.stepIndex > right.stepIndex ? left : right :
      // `prosemirror-changeset` uses null as its incompatible-data sentinel even
      // though its public combine callback is typed as returning Data.
      null;
    }, tokenEncoder).addSteps(steppedDoc, stepMaps, simplifiedStepAttributions.map(function (attribution, stepIndex) {
      return _objectSpread(_objectSpread({}, attribution), {}, {
        stepIndex: stepIndex
      });
    }));
    contentChanges = changeset.changes;
    attributedChanges = diffType === 'step' ? diffBySteps(originalDoc, simplifiedSteps, simplifiedStepAttributions) : _toConsumableArray(changeset.changes);
    // The attributed pipeline gives no guarantee that its output ranges are disjoint — see
    // `collapseOverlappingChanges`.
    var uncollapsedChanges = getAttributedChanges({
      changeset: changeset,
      originalDoc: originalDoc,
      steppedDoc: steppedDoc,
      diffType: diffType,
      tr: tr,
      attributedChanges: attributedChanges
    });
    changes = collapseOverlappingChanges(uncollapsedChanges, tr.doc);

    // Still colours-only: a tags-only diff keeps the plain colour scheme.
    if (attributionColoringEnabled) {
      attributionColors = createAttributionColorMap(simplifiedStepAttributions, contributors);
    }
  } else {
    var _changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
    contentChanges = _changeset.changes;
    changes = getChanges({
      changeset: _changeset,
      originalDoc: originalDoc,
      steppedDoc: steppedDoc,
      diffType: diffType,
      tr: tr,
      steps: simplifiedSteps,
      intl: intl,
      smartThresholds: smartThresholds
    });
  }
  var decorations = [];

  /**
   * If showIndicators is on, we create an anchor widget here to mark the doc margin.
   */
  if (showIndicators) {
    decorations.push(createDocMarginAnchorWidget());
  }

  // Our default operations are insertions, so it should match the opposite of isInverted.
  var isInserted = !isInverted;
  changes = groupDeletedColumnChanges(changes, originalDoc, tr.doc, new Mapping(stepMaps), isInverted);
  // A whole-table change owns every mark and attribute range inside its bounds, so those ranges
  // do not also get their own decoration.
  var tableChanges = changes.filter(function (change) {
    return change.deletedColumns !== undefined;
  });
  var isHandledByTable = function isHandledByTable(from, to) {
    return tableChanges.some(function (change) {
      return from >= change.fromB && to <= change.toB;
    });
  };
  var createDecorationsForChange = function createDecorationsForChange(change) {
    // Patch 2 makes all decorations generated for one replacement share this anchor. Their own
    // IDs still identify their top/bottom bounds, while this ID makes their bars agree on one
    // left edge. When the gate is off, factories retain their existing per-decoration anchors.
    var leftAnchorId = fg('platform_editor_ai_show_diff_patch_2') ? "replacement-".concat(change.fromA, "-").concat(change.toA, "-").concat(change.fromB, "-").concat(change.toB) : undefined;
    var changeColorScheme = attributionColors ? getColorSchemeForChange(change, attributedChanges, attributionColors, colorScheme) : colorScheme;
    var attributionKey = showContributorTags ? getAttributionKeyForChange(change, attributedChanges) : undefined;
    var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
    var deletedColumns = change.deletedColumns;
    // On an inverted diff the table that lost columns is the one in `tr.doc`, so the column
    // indices address it directly and each label anchors inside its own cell.
    var table = tr.doc.nodeAt(change.fromB);
    if (isInverted && !hideDeletedDiffs && deletedColumns !== null && deletedColumns !== void 0 && deletedColumns.length && (table === null || table === void 0 ? void 0 : table.type.name) === 'table') {
      var tableMap = TableMap.get(table);
      deletedColumns.forEach(function (column) {
        var offset = tableMap.positionAt(0, column, table);
        decorations.push(Decoration.widget(change.fromB + CELL_CONTENT_OFFSET + offset, function () {
          return createRemovedLozenge(intl, isActive, changeColorScheme, true);
        }, {
          side: -1
        }));
      });
    }

    // The deleted side of a mark-only change is a byte-identical copy of the new text, so it is
    // suppressed in the clean view. `hideDeletedDiffs` is load-bearing: when false the reviewer
    // asked to compare against the original, and the deleted side is the only place the
    // previous formatting is visible.
    var isMarkOnly = shouldHideMarkOnlyDeletions && hideDeletedDiffs && change.deleted.length > 0 && isMarkOnlyChange({
      change: change,
      originalDoc: originalDoc,
      newDoc: tr.doc
    });

    // The deleted side of a change over a node's open token is an attribute state rather than
    // content, so there is nothing for the deleted-content widget to draw. Decided here, with
    // the other deleted-side suppressions, so the widget is never asked for a slice it can only
    // render as an empty copy of the block (EDITOR-8912). Its inserted side is untouched, and
    // the node's content change is a change of its own.
    var hasNoDeletedContent = fg('platform_editor_ai_show_diff_patch_1') && change.deleted.length > 0 && isOpenTokenOnlyChange({
      change: change,
      originalDoc: originalDoc
    });

    // Hoisted because it decides BOTH where the deleted widget is anchored and — since the
    // widget pins whichever end of the range it sits at — how the indicator anchors below are
    // allowed to move.
    var isDeletedWidgetBelow = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && isDeletedContentPlacedBelow({
      change: change,
      deletedDiffPlacement: deletedDiffPlacement,
      inlineDeletedDiffPlacement: inlineDeletedDiffPlacement
    });
    if (change.inserted.length > 0) {
      // On an inverted diff the inserted side is visually the deleted side.
      var shouldHideDeleted = isInverted && hideDeletedDiffs;

      // A removed blank line — an empty paragraph or heading — has nothing to mark up. On an
      // inverted diff, the shape AI suggested edits renders, the block is still in the displayed
      // document, so the removal lands here on the inserted side rather than on the deleted side,
      // and both the inline and the block decoration paint nothing: an empty textblock has no
      // inline content to cover, and block decorations return no style for a paragraph or heading.
      // The reviewer sees an unmarked blank line. Stand the struck-through return glyph in for each
      // one, anchored inside the block so it renders on the blank line itself.
      //
      // One decoration per blank line rather than one per change: a run of adjacent blank lines is
      // reported as a single change spanning all of them.
      //
      // Withheld in the clean view, where removals are hidden rather than struck through.
      if (fg('platform_editor_ai_show_diff_patch_2') && !isInserted && !shouldHideDeleted) {
        var _iterator3 = _createForOfIteratorHelper(emptyTextBlockContentPositions(tr.doc, change.fromB, change.toB)),
          _step3;
        try {
          for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
            var pos = _step3.value;
            decorations.push(createDeletedLineBreakDecoration({
              attributionKey: attributionKey,
              colorScheme: changeColorScheme,
              isActive: isActive,
              pos: pos,
              reveal: reveal,
              side: -1
            }));
          }
        } catch (err) {
          _iterator3.e(err);
        } finally {
          _iterator3.f();
        }
      }

      // For `smart` NODE-level promotions the change range spans a whole container
      // (e.g. an entire list/table/layout, using outer node bounds). Applying a SINGLE
      // inline decoration across that whole range would paint the inserted style across
      // block boundaries and structural gaps (list markers, empty item slots), producing
      // phantom "empty" rows above the real content. But skipping the inline highlight
      // entirely leaves added text-bearing blocks (paragraphs/headings inside the added
      // container) without the inserted background+underline, because block decorations
      // return no style for paragraph/heading. So for node-level smart changes we instead
      // emit ONE inline decoration per leaf text-bearing block within the range — the text
      // gets highlighted, and structural gaps never do.
      var isSmartNodeLevel = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && smartChangeLevel(change) === 'node';
      // For a large inserted table, skip the per-cell inline decorations that
      // freeze the browser on re-render. Only active when Review moment is
      // eligible (xstate + M1).
      var useCoarseTableDecoration = isSmartNodeLevel && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true && isLargeInsertedTableRange(tr.doc, change.fromB, change.toB);
      // Whether the deleted-content widget will actually be rendered for this
      // change. Used to decide if indicator anchor positions should be adjusted
      // inward — when the widget is present the anchor must stay at the block
      // boundary to keep the indicator bar continuous with the deleted content.
      var willRenderDeletedWidget = change.deleted.length > 0 && !isMarkOnly && !shouldHideDeletedSide({
        change: change,
        hideDeletedDiffs: hideDeletedDiffs,
        isInverted: isInverted
      });
      if (isSmartNodeLevel) {
        // For a large inserted table, skip the O(cells) inline decorations inside the
        // table (the cell overlays added below provide the highlight without the
        // expensive content re-render); leaf blocks outside the table still get theirs.
        var coarseTables = useCoarseTableDecoration ? tableRanges(tr.doc, change.fromB, change.toB) : [];
        var _iterator4 = _createForOfIteratorHelper(leafTextblockRanges(tr.doc, change.fromB, change.toB)),
          _step4;
        try {
          for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
            var _step4$value = _slicedToArray(_step4.value, 2),
              from = _step4$value[0],
              to = _step4$value[1];
            if (useCoarseTableDecoration && isInsideTable(coarseTables, from)) {
              continue;
            }
            decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
              attributionKey: attributionKey,
              change: {
                fromB: from,
                toB: to
              },
              leftAnchorId: leftAnchorId,
              doc: tr.doc,
              colorScheme: changeColorScheme,
              isActive: isActive,
              isInserted: isInserted,
              shouldHideDeleted: shouldHideDeleted,
              showContributorTags: showContributorTags,
              showIndicators: showIndicators,
              hideAddedDiffsUnderline: hideAddedDiffsUnderline,
              hasDeletedWidget: willRenderDeletedWidget,
              isDeletedWidgetBelow: isDeletedWidgetBelow,
              reveal: reveal,
              tagMountContext: tagMountContext
            })));
          }
        } catch (err) {
          _iterator4.e(err);
        } finally {
          _iterator4.f();
        }
      } else {
        decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
          attributionKey: attributionKey,
          change: change,
          leftAnchorId: leftAnchorId,
          doc: tr.doc,
          colorScheme: changeColorScheme,
          isActive: isActive,
          isInserted: isInserted,
          shouldHideDeleted: shouldHideDeleted,
          showContributorTags: showContributorTags,
          showIndicators: showIndicators,
          hideAddedDiffsUnderline: hideAddedDiffsUnderline,
          hasDeletedWidget: willRenderDeletedWidget,
          isDeletedWidgetBelow: isDeletedWidgetBelow,
          reveal: reveal,
          tagMountContext: tagMountContext
        })));
      }
      decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
        // A deleted widget placed below anchors at the change's end, inside the very block
        // this would let the decoration claim.
        allowTrailingCloseTokens: !isDeletedWidgetBelow,
        attributionKey: attributionKey,
        doc: tr.doc,
        from: change.fromB,
        to: change.toB,
        // A split piece's own range no longer covers the container it opened (EDITOR-8932).
        nodeRanges: change.blockNodeRangesB,
        colorScheme: changeColorScheme,
        isInserted: isInserted,
        shouldHideDeleted: shouldHideDeleted,
        showContributorTags: showContributorTags,
        showIndicators: showIndicators,
        tagMountContext: tagMountContext,
        activeIndexPos: activeIndexPos,
        intl: intl,
        coarseTableCellsOnly: useCoarseTableDecoration,
        leftAnchorId: leftAnchorId
      })));
    }
    if (change.deleted.length > 0 && !isMarkOnly && !hasNoDeletedContent) {
      var _shouldHideDeleted = shouldHideDeletedSide({
        change: change,
        hideDeletedDiffs: hideDeletedDiffs,
        isInverted: isInverted
      });
      if (!_shouldHideDeleted) {
        decorations.push.apply(decorations, _toConsumableArray(createNodeChangedDecorationWidget({
          deletedColumns: deletedColumns,
          // The deleted side has no inline decoration, so the widget carries the actor.
          attributionKey: attributionKey,
          reveal: reveal,
          change: change,
          doc: originalDoc,
          nodeViewSerializer: nodeViewSerializer,
          colorScheme: changeColorScheme,
          newDoc: tr.doc,
          intl: intl,
          activeIndexPos: activeIndexPos,
          leftAnchorId: leftAnchorId,
          showContributorTags: showContributorTags,
          tagMountContext: tagMountContext,
          isInserted: !isInserted,
          hideAddedDiffsUnderline: hideAddedDiffsUnderline,
          placeBelow: isDeletedWidgetBelow,
          showIndicators: showIndicators
        })));
      }
    }
  };
  changes.forEach(function (change) {
    createDecorationsForChange(change);
  });
  // Compare the content outside replacement ranges at matching positions in both documents.
  // This preserves real formatting edits without highlighting an unchanged link mark reapplied
  // while editing its display text.
  var hasUnhighlightedMarkChanges = function hasUnhighlightedMarkChanges(_ref10) {
    var fromB = _ref10.fromB,
      toB = _ref10.toB;
    var from = fromB;
    var offset = 0;
    var differs = function differs(to) {
      return from < to && !originalDoc.slice(from + offset, to + offset).content.eq(tr.doc.slice(from, to).content);
    };
    var _iterator5 = _createForOfIteratorHelper(contentChanges),
      _step5;
    try {
      for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
        var change = _step5.value;
        if (change.fromB >= toB) {
          break;
        }
        if (change.toB > from) {
          if (differs(change.fromB)) {
            return true;
          }
          from = change.toB;
        }
        offset = change.toA - change.toB;
      }
    } catch (err) {
      _iterator5.e(err);
    } finally {
      _iterator5.f();
    }
    return differs(toB);
  };

  // The attribute-aware token encoder already reports mark-only changes as regular changes in the
  // smart path and the NCS version-history path. For NCS version history, only skip the step-range
  // highlight if the main diff covers it or the remaining content has unchanged formatting.
  getMarkChangeRanges(simplifiedSteps, simplifiedStepAttributions, fg('confluence_ncs_step_diffing_version_history') ? stepMaps : undefined).filter(function (_ref11) {
    var fromB = _ref11.fromB,
      toB = _ref11.toB;
    return !isHandledByTable(fromB, toB);
  }).filter(function (range) {
    return !fg('confluence_ncs_step_diffing_version_history') || !changes.some(function (change) {
      return change.fromB <= range.fromB && change.toB >= range.toB;
    }) && hasUnhighlightedMarkChanges(range);
  }).forEach(function (change) {
    var _attributionColors$ge, _attributionColors;
    var changeColorScheme = change.attributionKey ? (_attributionColors$ge = (_attributionColors = attributionColors) === null || _attributionColors === void 0 ? void 0 : _attributionColors.get(change.attributionKey)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : colorScheme : colorScheme;
    var attributionKey = showContributorTags ? change.attributionKey : undefined;
    var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
    decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
      attributionKey: attributionKey,
      change: change,
      colorScheme: changeColorScheme,
      doc: tr.doc,
      isActive: isActive,
      isInserted: true,
      reveal: reveal,
      showContributorTags: showContributorTags,
      tagMountContext: tagMountContext
    })));
  });
  getAttrChangeRanges(tr.doc, attrStepContexts, originalDoc).filter(function (_ref12) {
    var fromB = _ref12.fromB,
      toB = _ref12.toB;
    return !isHandledByTable(fromB, toB);
  }).filter(function (_ref13) {
    var fromB = _ref13.fromB,
      toB = _ref13.toB,
      isInline = _ref13.isInline;
    return !fg('confluence_ncs_step_diffing_version_history') || !isInline || !changes.some(function (change) {
      return change.fromB === fromB && change.toB === toB;
    });
  }).forEach(function (change) {
    var _attributionColors$ge2, _attributionColors2;
    var changeColorScheme = change.attributionKey ? (_attributionColors$ge2 = (_attributionColors2 = attributionColors) === null || _attributionColors2 === void 0 ? void 0 : _attributionColors2.get(change.attributionKey)) !== null && _attributionColors$ge2 !== void 0 ? _attributionColors$ge2 : colorScheme : colorScheme;
    var attributionKey = showContributorTags ? change.attributionKey : undefined;
    if (change.isInline) {
      // Inline nodes (e.g. date, emoji, mention, status) need an inline decoration rather than a block decoration
      var isActive = isRangeActive(activeIndexPos, change.fromB, change.toB, attributionKey);
      decorations.push.apply(decorations, _toConsumableArray(createInlineChangedDecoration({
        attributionKey: attributionKey,
        change: change,
        colorScheme: changeColorScheme,
        doc: tr.doc,
        isActive: isActive,
        isInserted: true,
        isAtomicInlineNode: true,
        inlineNodeName: change.inlineNodeName,
        showContributorTags: showContributorTags,
        tagMountContext: tagMountContext,
        // Suppress the border-bottom underline for atomic inline nodeviews —
        // it doesn't render on custom nodeview DOM elements and is unnecessary noise.
        hideAddedDiffsUnderline: true
      })));
      // If we have the original node position, also render the old node as a "deleted" widget
      // so the user can see what it looked like before the change.
      if (change.fromA !== undefined && change.toA !== undefined) {
        decorations.push.apply(decorations, _toConsumableArray(createNodeChangedDecorationWidget({
          change: {
            fromA: change.fromA,
            toA: change.toA,
            fromB: change.fromB,
            toB: change.toB,
            deleted: []
          },
          doc: originalDoc,
          attributionKey: attributionKey,
          nodeViewSerializer: nodeViewSerializer,
          colorScheme: changeColorScheme,
          newDoc: tr.doc,
          intl: intl,
          activeIndexPos: activeIndexPos,
          isInserted: false,
          showContributorTags: showContributorTags,
          tagMountContext: tagMountContext
        })));
      }
    } else {
      decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration({
        doc: tr.doc,
        from: change.fromB,
        to: change.toB,
        attributionKey: attributionKey,
        colorScheme: changeColorScheme,
        isInserted: true,
        activeIndexPos: activeIndexPos,
        intl: intl,
        showContributorTags: showContributorTags,
        showIndicators: showIndicators,
        tagMountContext: tagMountContext
      })));
      // If the original node position is known (e.g. a panel type change), also render
      // the old block node as a "deleted" widget so the reviewer sees the before/after.
      var isSmartNodeLevelAttrChange = diffType === 'smart' && fg('platform_editor_ai_smart_diff') && expValEqualsNoExposure('platform_editor_ai_xstate_migration', 'isEnabled', true) && UNSAFE_expValNoExposure('platform_editor_ai_streaming_ux_experience_m1', 'isEnabled', false) === true;
      if (isSmartNodeLevelAttrChange && change.fromA !== undefined && change.toA !== undefined) {
        var placeBelow = deletedDiffPlacement === 'bottom';
        decorations.push.apply(decorations, _toConsumableArray(createNodeChangedDecorationWidget({
          change: {
            fromA: change.fromA,
            toA: change.toA,
            fromB: change.fromB,
            toB: change.toB,
            deleted: []
          },
          doc: originalDoc,
          nodeViewSerializer: nodeViewSerializer,
          attributionKey: attributionKey,
          colorScheme: changeColorScheme,
          newDoc: tr.doc,
          intl: intl,
          activeIndexPos: activeIndexPos,
          isInserted: false,
          hideAddedDiffsUnderline: hideAddedDiffsUnderline,
          placeBelow: placeBelow,
          showIndicators: showIndicators,
          showContributorTags: showContributorTags,
          tagMountContext: tagMountContext
        })));
      }
    }
  });
  var decorationSet = DecorationSet.empty.add(tr.doc, decorations);
  return {
    contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors, activeIndexPos,
    // The stops the step buttons walk, so a stop cannot hold a second tag for one
    // contributor that navigation can never reach.
    getScrollableDecorations(decorationSet, tr.doc)) : [],
    decorations: decorationSet,
    diffDescriptors: extractDiffDescriptors(decorationSet)
  };
};
export var calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner,
// Cache results unless relevant inputs change. `tagMountContext` is deliberately absent: it is
// built once per plugin instance, so it can never be the reason a diff needs recalculating.
function (_ref14, _ref15) {
  var _ref18;
  var _ref16 = _slicedToArray(_ref14, 1),
    _ref16$ = _ref16[0],
    pluginState = _ref16$.pluginState,
    state = _ref16$.state,
    colorScheme = _ref16$.colorScheme,
    intl = _ref16$.intl,
    activeIndexPos = _ref16$.activeIndexPos,
    isInverted = _ref16$.isInverted,
    diffType = _ref16$.diffType,
    hideDeletedDiffs = _ref16$.hideDeletedDiffs,
    hideAddedDiffsUnderline = _ref16$.hideAddedDiffsUnderline,
    showIndicators = _ref16$.showIndicators,
    smartThresholds = _ref16$.smartThresholds,
    deletedDiffPlacement = _ref16$.deletedDiffPlacement,
    inlineDeletedDiffPlacement = _ref16$.inlineDeletedDiffPlacement,
    reveal = _ref16$.reveal;
  var _ref17 = _slicedToArray(_ref15, 1),
    _ref17$ = _ref17[0],
    lastPluginState = _ref17$.pluginState,
    lastState = _ref17$.state,
    lastColorScheme = _ref17$.colorScheme,
    lastIntl = _ref17$.intl,
    lastActiveIndexPos = _ref17$.activeIndexPos,
    lastIsInverted = _ref17$.isInverted,
    lastDiffType = _ref17$.diffType,
    lastHideDeletedDiffs = _ref17$.hideDeletedDiffs,
    lastHideAddedDiffsUnderline = _ref17$.hideAddedDiffsUnderline,
    lastShowIndicators = _ref17$.showIndicators,
    lastSmartThresholds = _ref17$.smartThresholds,
    lastDeletedDiffPlacement = _ref17$.deletedDiffPlacement,
    lastInlineDeletedDiffPlacement = _ref17$.inlineDeletedDiffPlacement,
    lastReveal = _ref17$.reveal;
  var originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
  return (_ref18 = colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isInverted === lastIsInverted && diffType === lastDiffType && isEqual(activeIndexPos, lastActiveIndexPos) && originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && isEqual(pluginState.stepAttributions, lastPluginState.stepAttributions) &&
  // Contributors can arrive after the first calculation; without this, tags never appear.
  isEqual(pluginState.contributors, lastPluginState.contributors) && state.doc.eq(lastState.doc) && hideDeletedDiffs === lastHideDeletedDiffs && hideAddedDiffsUnderline === lastHideAddedDiffsUnderline && showIndicators === lastShowIndicators && isEqual(smartThresholds, lastSmartThresholds) && deletedDiffPlacement === lastDeletedDiffPlacement && inlineDeletedDiffPlacement === lastInlineDeletedDiffPlacement &&
  // Turning the reveal on or off changes the emitted styles, so a cached result from the
  // other setting would leave the diff painted in the wrong resting state.
  isEqual(reveal, lastReveal)) !== null && _ref18 !== void 0 ? _ref18 : false;
});