@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
223 lines (222 loc) • 7.99 kB
JavaScript
// eslint-disable-next-line @atlassian/tangerine/import/entry-points
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { areDocsEqualByBlockStructureAndText } from '../areDocsEqualByBlockStructureAndText';
import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
import { groupChangesByBlock } from './groupChangesByBlock';
import { optimizeChanges } from './optimizeChanges';
import { simplifySteps } from './simplifySteps';
const getChanges = ({
changeset,
originalDoc,
steppedDoc,
diffType,
tr
}) => {
if (diffType === 'block' && expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc);
}
const changes = simplifyChanges(changeset.changes, tr.doc);
return optimizeChanges(changes);
};
const calculateNodesForBlockDecoration = ({
doc,
from,
to,
colorScheme,
isInserted = true,
activeIndexPos
}) => {
const decorations = [];
// Iterate over the document nodes within the range
doc.nodesBetween(from, to, (node, pos) => {
if (node.isBlock && (!expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) || pos + node.nodeSize <= to)) {
const nodeEnd = pos + node.nodeSize;
const isActive = activeIndexPos && pos === activeIndexPos.from && nodeEnd === activeIndexPos.to;
const decoration = createBlockChangedDecoration({
change: {
from: pos,
to: nodeEnd,
name: node.type.name
},
colorScheme,
isInserted,
isActive
});
if (decoration) {
decorations.push(decoration);
}
}
});
return decorations;
};
const calculateDiffDecorationsInner = ({
state,
pluginState,
nodeViewSerializer,
colorScheme,
intl,
activeIndexPos,
api,
isInverted = false,
diffType = 'inline'
}) => {
const {
originalDoc,
steps
} = pluginState;
if (!originalDoc || !pluginState.isDisplayingChanges) {
return DecorationSet.empty;
}
const {
tr
} = state;
let steppedDoc = originalDoc;
const attrSteps = [];
const simplifiedSteps = simplifySteps(steps, originalDoc);
const stepMaps = [];
for (const step of simplifiedSteps) {
const result = step.apply(steppedDoc);
if (result.failed === null && result.doc) {
if (stepIsValidAttrChange(step, steppedDoc, result.doc)) {
attrSteps.push(step);
}
stepMaps.push(step.getMap());
steppedDoc = result.doc;
}
}
// 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)) {
const recoveredViaContentEquality = fg('platform_editor_show_diff_equality_fallback') ? areDocsEqualByBlockStructureAndText(steppedDoc, tr.doc) : undefined;
if (expValEquals('platform_editor_are_nodes_equal_ignore_mark_order', 'isEnabled', true)) {
var _api$analytics;
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.fireAnalyticsEvent({
eventType: 'track',
action: 'nodesNotEqual',
actionSubject: 'showDiff',
attributes: {
docSizeEqual: steppedDoc.nodeSize === tr.doc.nodeSize,
colorScheme,
recoveredViaContentEquality
}
});
}
if (fg('platform_editor_show_diff_equality_fallback')) {
if (!recoveredViaContentEquality) {
return DecorationSet.empty;
}
} else {
return DecorationSet.empty;
}
}
const changeset = ChangeSet.create(originalDoc).addSteps(steppedDoc, stepMaps, tr.doc);
const changes = getChanges({
changeset,
originalDoc,
steppedDoc,
diffType,
tr
});
const decorations = [];
changes.forEach(change => {
const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
// Our default operations are insertions, so it should match the opposite of isInverted.
const isInserted = !isInverted;
if (change.inserted.length > 0) {
decorations.push(createInlineChangedDecoration({
change,
colorScheme,
isActive,
...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && {
isInserted
})
}));
decorations.push(...calculateNodesForBlockDecoration({
doc: tr.doc,
from: change.fromB,
to: change.toB,
colorScheme,
...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && {
isInserted
}),
activeIndexPos,
intl
}));
}
if (change.deleted.length > 0) {
const decoration = createNodeChangedDecorationWidget({
change,
doc: originalDoc,
nodeViewSerializer,
colorScheme,
newDoc: tr.doc,
intl,
activeIndexPos,
...(expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) && {
isInserted: !isInserted
})
});
if (decoration) {
decorations.push(...decoration);
}
}
});
getMarkChangeRanges(steps).forEach(change => {
const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
decorations.push(createInlineChangedDecoration({
change,
colorScheme,
isActive,
isInserted: true
}));
});
getAttrChangeRanges(tr.doc, attrSteps).forEach(change => {
decorations.push(...calculateNodesForBlockDecoration({
doc: tr.doc,
from: change.fromB,
to: change.toB,
colorScheme,
isInserted: true,
activeIndexPos,
intl
}));
});
return DecorationSet.empty.add(tr.doc, decorations);
};
export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner,
// Cache results unless relevant inputs change
([{
pluginState,
state,
colorScheme,
intl,
activeIndexPos,
isInverted,
diffType
}], [{
pluginState: lastPluginState,
state: lastState,
colorScheme: lastColorScheme,
intl: lastIntl,
activeIndexPos: lastActiveIndexPos,
isInverted: lastIsInverted,
diffType: lastDiffType
}]) => {
var _ref2;
const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
var _ref;
return (_ref = colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isInverted === lastIsInverted && diffType === lastDiffType && isEqual(activeIndexPos, lastActiveIndexPos) && originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc)) !== null && _ref !== void 0 ? _ref : false;
}
return (_ref2 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isEqual(activeIndexPos, lastActiveIndexPos)) !== null && _ref2 !== void 0 ? _ref2 : false;
});