@atlaskit/editor-plugin-show-diff 14.0.3 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +204 -101
  3. package/dist/cjs/pm-plugins/calculateDiff/diffBySteps.js +20 -9
  4. package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +89 -0
  5. package/dist/cjs/pm-plugins/calculateDiff/simplifySteps.js +81 -4
  6. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +68 -0
  7. package/dist/cjs/pm-plugins/decorations/colorSchemes/schemes.js +26 -2
  8. package/dist/cjs/pm-plugins/decorations/colorSchemes/types.js +5 -1
  9. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +7 -2
  10. package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +2 -2
  11. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +4 -2
  12. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +14 -10
  13. package/dist/cjs/pm-plugins/main.js +1 -0
  14. package/dist/cjs/showDiffPlugin.js +36 -14
  15. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +118 -26
  16. package/dist/es2019/pm-plugins/calculateDiff/diffBySteps.js +17 -9
  17. package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +64 -0
  18. package/dist/es2019/pm-plugins/calculateDiff/simplifySteps.js +64 -1
  19. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +37 -0
  20. package/dist/es2019/pm-plugins/decorations/colorSchemes/schemes.js +19 -2
  21. package/dist/es2019/pm-plugins/decorations/colorSchemes/types.js +1 -0
  22. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +6 -3
  23. package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +3 -3
  24. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +6 -3
  25. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +12 -9
  26. package/dist/es2019/pm-plugins/main.js +1 -0
  27. package/dist/es2019/showDiffPlugin.js +23 -1
  28. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +205 -102
  29. package/dist/esm/pm-plugins/calculateDiff/diffBySteps.js +20 -9
  30. package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +82 -0
  31. package/dist/esm/pm-plugins/calculateDiff/simplifySteps.js +79 -5
  32. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +61 -0
  33. package/dist/esm/pm-plugins/decorations/colorSchemes/schemes.js +24 -1
  34. package/dist/esm/pm-plugins/decorations/colorSchemes/types.js +1 -0
  35. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +8 -3
  36. package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +3 -3
  37. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +5 -3
  38. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +14 -9
  39. package/dist/esm/pm-plugins/main.js +1 -0
  40. package/dist/esm/showDiffPlugin.js +36 -14
  41. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  42. package/dist/types/pm-plugins/calculateDiff/diffBySteps.d.ts +2 -1
  43. package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +7 -0
  44. package/dist/types/pm-plugins/calculateDiff/simplifySteps.d.ts +10 -0
  45. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +12 -0
  46. package/dist/types/pm-plugins/decorations/colorSchemes/schemes.d.ts +5 -3
  47. package/dist/types/pm-plugins/decorations/colorSchemes/types.d.ts +6 -1
  48. package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +2 -1
  49. package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +2 -1
  50. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +2 -1
  51. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +2 -1
  52. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +2 -1
  53. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.d.ts +2 -1
  54. package/dist/types/pm-plugins/main.d.ts +3 -1
  55. package/dist/types/showDiffPluginType.d.ts +29 -1
  56. package/package.json +3 -3
@@ -11,6 +11,7 @@ import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorati
11
11
  import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
12
12
  import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
13
13
  import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
14
+ import { createAttributionColorMap, getAttributionKey, getColorSchemeForChange, isAttributionColoringEnabled } from '../decorations/colorSchemes/attributions';
14
15
  import { extractDiffDescriptors } from '../decorations/decorationKeys';
15
16
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
16
17
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
@@ -19,7 +20,8 @@ import { attrAwareTokenEncoder } from './attrAwareTokenEncoder';
19
20
  import { diffBySteps } from './diffBySteps';
20
21
  import { groupChangesByBlock } from './groupChangesByBlock';
21
22
  import { optimizeChanges } from './optimizeChanges';
22
- import { simplifySteps } from './simplifySteps';
23
+ import { simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
24
+ import { simplifySteps, simplifyStepsWithAttribution } from './simplifySteps';
23
25
  import { classifySmartChanges } from './smart/classifySmartChanges';
24
26
  import { smartChangeLevel } from './smart/helpers';
25
27
  const getChanges = ({
@@ -58,6 +60,35 @@ const getChanges = ({
58
60
  const changes = simplifyChanges(changeset.changes, tr.doc);
59
61
  return optimizeChanges(changes);
60
62
  };
63
+ const getAttributedChanges = ({
64
+ attributedChanges,
65
+ changeset,
66
+ originalDoc,
67
+ steppedDoc,
68
+ diffType,
69
+ tr,
70
+ intl,
71
+ smartThresholds
72
+ }) => {
73
+ if (isExtendedEnabled(diffType)) {
74
+ if (diffType === 'smart' && fg('platform_editor_ai_smart_diff')) {
75
+ return classifySmartChanges({
76
+ changes: simplifyChangesWithAttribution(attributedChanges, tr.doc),
77
+ originalDoc,
78
+ newDoc: tr.doc,
79
+ locale: intl.locale,
80
+ thresholds: smartThresholds
81
+ });
82
+ }
83
+ if (diffType === 'step') {
84
+ return attributedChanges;
85
+ }
86
+ if (diffType === 'block') {
87
+ return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc);
88
+ }
89
+ }
90
+ return simplifyChangesWithAttribution(attributedChanges, tr.doc);
91
+ };
61
92
 
62
93
  /**
63
94
  * Collect the inline-content ranges of every leaf text-bearing block (paragraph, heading, …)
@@ -101,6 +132,21 @@ const isLargeInsertedTableRange = (doc, from, to) => {
101
132
  });
102
133
  return containsTable && leafCount > LARGE_TABLE_LEAF_THRESHOLD;
103
134
  };
135
+
136
+ // Collect the [from, to) ranges of every `table` node overlapping `[from, to)`. The coarse
137
+ // large-table path must only suppress decorations inside these ranges
138
+ const tableRanges = (doc, from, to) => {
139
+ const ranges = [];
140
+ doc.nodesBetween(from, to, (node, pos) => {
141
+ if (node.type.name === 'table') {
142
+ ranges.push([pos, pos + node.nodeSize]);
143
+ return false;
144
+ }
145
+ return true;
146
+ });
147
+ return ranges;
148
+ };
149
+ const isInsideTable = (tables, pos) => tables.some(([tFrom, tTo]) => pos >= tFrom && pos < tTo);
104
150
  const calculateNodesForBlockDecoration = ({
105
151
  doc,
106
152
  from,
@@ -114,11 +160,13 @@ const calculateNodesForBlockDecoration = ({
114
160
  coarseTableCellsOnly = false
115
161
  }) => {
116
162
  const decorations = [];
163
+ // Coarse path: inside the large table, only cell/header overlays are kept — the
164
+ // table/row/paragraph decorations there are redundant and cause expensive per-cell
165
+ // re-render. Blocks outside the table keep their normal decorations.
166
+ const coarseTables = coarseTableCellsOnly ? tableRanges(doc, from, to) : [];
117
167
  // Iterate over the document nodes within the range
118
168
  doc.nodesBetween(from, to, (node, pos) => {
119
- // Coarse path: only cell/header overlays are visible; table/row/paragraph
120
- // decorations are redundant and cause expensive per-cell re-render.
121
- if (coarseTableCellsOnly) {
169
+ if (coarseTableCellsOnly && isInsideTable(coarseTables, pos)) {
122
170
  const name = node.type.name;
123
171
  if (name !== 'tableCell' && name !== 'tableHeader') {
124
172
  return;
@@ -189,6 +237,7 @@ const calculateDiffDecorationsInner = ({
189
237
  const {
190
238
  originalDoc,
191
239
  steps,
240
+ stepAttributions,
192
241
  isDisplayingChanges
193
242
  } = pluginState;
194
243
  if (!originalDoc || !isDisplayingChanges) {
@@ -205,8 +254,21 @@ const calculateDiffDecorationsInner = ({
205
254
  tr
206
255
  } = state;
207
256
  let steppedDoc = originalDoc;
257
+ const attributionColoringEnabled = isAttributionColoringEnabled(stepAttributions);
258
+ let simplifiedSteps;
259
+ let simplifiedStepAttributions = [];
260
+ if (attributionColoringEnabled) {
261
+ const simplifiedStepsWithAttribution = simplifyStepsWithAttribution(steps, stepAttributions !== null && stepAttributions !== void 0 ? stepAttributions : [], originalDoc);
262
+ simplifiedSteps = simplifiedStepsWithAttribution.map(({
263
+ step
264
+ }) => step);
265
+ simplifiedStepAttributions = simplifiedStepsWithAttribution.map(({
266
+ stepAttribution
267
+ }) => stepAttribution);
268
+ } else {
269
+ simplifiedSteps = simplifySteps(steps, originalDoc);
270
+ }
208
271
  const attrSteps = [];
209
- const simplifiedSteps = simplifySteps(steps, originalDoc);
210
272
  const stepMaps = [];
211
273
  for (const step of simplifiedSteps) {
212
274
  const result = step.apply(steppedDoc);
@@ -245,17 +307,42 @@ const calculateDiffDecorationsInner = ({
245
307
  // gated with it; other diff types keep the library default so their output is
246
308
  // unchanged.
247
309
  const tokenEncoder = diffType === 'smart' && fg('platform_editor_ai_smart_diff') ? attrAwareTokenEncoder : undefined;
248
- const changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
249
- const changes = getChanges({
250
- changeset,
251
- originalDoc,
252
- steppedDoc,
253
- diffType,
254
- tr,
255
- steps,
256
- intl,
257
- smartThresholds
258
- });
310
+ let changes;
311
+ let attributedChanges = [];
312
+ let attributionColors;
313
+ if (attributionColoringEnabled) {
314
+ const changeset = ChangeSet.create(originalDoc, (left, right) => getAttributionKey(left) === getAttributionKey(right) ? left.stepIndex > right.stepIndex ? left : right :
315
+ // `prosemirror-changeset` uses null as its incompatible-data sentinel even
316
+ // though its public combine callback is typed as returning Data.
317
+ null, tokenEncoder).addSteps(steppedDoc, stepMaps, simplifiedStepAttributions.map((attribution, stepIndex) => ({
318
+ ...attribution,
319
+ stepIndex
320
+ })));
321
+ attributedChanges = diffBySteps(originalDoc, simplifiedSteps, simplifiedStepAttributions);
322
+ changes = getAttributedChanges({
323
+ changeset,
324
+ originalDoc,
325
+ steppedDoc,
326
+ diffType,
327
+ tr,
328
+ attributedChanges,
329
+ intl,
330
+ smartThresholds
331
+ });
332
+ attributionColors = createAttributionColorMap(simplifiedStepAttributions);
333
+ } else {
334
+ const changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
335
+ changes = getChanges({
336
+ changeset,
337
+ originalDoc,
338
+ steppedDoc,
339
+ diffType,
340
+ tr,
341
+ steps: simplifiedSteps,
342
+ intl,
343
+ smartThresholds
344
+ });
345
+ }
259
346
  const decorations = [];
260
347
 
261
348
  /**
@@ -268,6 +355,7 @@ const calculateDiffDecorationsInner = ({
268
355
  // Our default operations are insertions, so it should match the opposite of isInverted.
269
356
  const isInserted = !isInverted;
270
357
  const createDecorationsForChange = change => {
358
+ const changeColorScheme = attributionColors ? getColorSchemeForChange(change, attributedChanges, attributionColors, colorScheme) : colorScheme;
271
359
  const isActive = activeIndexPos && change.fromB === activeIndexPos.from && change.toB === activeIndexPos.to;
272
360
 
273
361
  // Hoisted because it decides BOTH where the deleted widget is anchored and — since the
@@ -301,18 +389,22 @@ const calculateDiffDecorationsInner = ({
301
389
  // inward — when the widget is present the anchor must stay at the block
302
390
  // boundary to keep the indicator bar continuous with the deleted content.
303
391
  const willRenderDeletedWidget = change.deleted.length > 0 && !(isExtendedEnabled(diffType) && !isInverted && hideDeletedDiffs && change.inserted.length > 0);
304
- if (useCoarseTableDecoration) {
305
- // Skip the O(cells) inline decorations; the cell overlays (added below)
306
- // provide the highlight without the expensive content re-render.
307
- } else if (isSmartNodeLevel) {
392
+ if (isSmartNodeLevel) {
393
+ // For a large inserted table, skip the O(cells) inline decorations inside the
394
+ // table (the cell overlays added below provide the highlight without the
395
+ // expensive content re-render); leaf blocks outside the table still get theirs.
396
+ const coarseTables = useCoarseTableDecoration ? tableRanges(tr.doc, change.fromB, change.toB) : [];
308
397
  for (const [from, to] of leafTextblockRanges(tr.doc, change.fromB, change.toB)) {
398
+ if (useCoarseTableDecoration && isInsideTable(coarseTables, from)) {
399
+ continue;
400
+ }
309
401
  decorations.push(...createInlineChangedDecoration({
310
402
  change: {
311
403
  fromB: from,
312
404
  toB: to
313
405
  },
314
406
  doc: tr.doc,
315
- colorScheme,
407
+ colorScheme: changeColorScheme,
316
408
  isActive,
317
409
  diffType,
318
410
  ...(isExtendedEnabled(diffType) && {
@@ -329,7 +421,7 @@ const calculateDiffDecorationsInner = ({
329
421
  decorations.push(...createInlineChangedDecoration({
330
422
  change,
331
423
  doc: tr.doc,
332
- colorScheme,
424
+ colorScheme: changeColorScheme,
333
425
  isActive,
334
426
  diffType,
335
427
  ...(isExtendedEnabled(diffType) && {
@@ -346,7 +438,7 @@ const calculateDiffDecorationsInner = ({
346
438
  doc: tr.doc,
347
439
  from: change.fromB,
348
440
  to: change.toB,
349
- colorScheme,
441
+ colorScheme: changeColorScheme,
350
442
  ...(isExtendedEnabled(diffType) && {
351
443
  isInserted,
352
444
  shouldHideDeleted,
@@ -365,7 +457,7 @@ const calculateDiffDecorationsInner = ({
365
457
  change,
366
458
  doc: originalDoc,
367
459
  nodeViewSerializer,
368
- colorScheme,
460
+ colorScheme: changeColorScheme,
369
461
  newDoc: tr.doc,
370
462
  intl,
371
463
  activeIndexPos,
@@ -511,7 +603,7 @@ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner
511
603
  const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
512
604
  if (isExtendedEnabled(diffType)) {
513
605
  var _ref;
514
- 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) && hideDeletedDiffs === lastHideDeletedDiffs && hideAddedDiffsUnderline === lastHideAddedDiffsUnderline && showIndicators === lastShowIndicators && isEqual(smartThresholds, lastSmartThresholds) && deletedDiffPlacement === lastDeletedDiffPlacement && inlineDeletedDiffPlacement === lastInlineDeletedDiffPlacement) !== null && _ref !== void 0 ? _ref : false;
606
+ return (_ref = colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isInverted === lastIsInverted && diffType === lastDiffType && isEqual(activeIndexPos, lastActiveIndexPos) && originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && isEqual(pluginState.stepAttributions, lastPluginState.stepAttributions) && state.doc.eq(lastState.doc) && hideDeletedDiffs === lastHideDeletedDiffs && hideAddedDiffsUnderline === lastHideAddedDiffsUnderline && showIndicators === lastShowIndicators && isEqual(smartThresholds, lastSmartThresholds) && deletedDiffPlacement === lastDeletedDiffPlacement && inlineDeletedDiffPlacement === lastInlineDeletedDiffPlacement) !== null && _ref !== void 0 ? _ref : false;
515
607
  }
516
- return (_ref2 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isEqual(activeIndexPos, lastActiveIndexPos) && hideDeletedDiffs === lastHideDeletedDiffs) !== null && _ref2 !== void 0 ? _ref2 : false;
608
+ return (_ref2 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && isEqual(pluginState.stepAttributions, lastPluginState.stepAttributions) && state.doc.eq(lastState.doc) && colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isEqual(activeIndexPos, lastActiveIndexPos) && hideDeletedDiffs === lastHideDeletedDiffs) !== null && _ref2 !== void 0 ? _ref2 : false;
517
609
  });
@@ -1,6 +1,7 @@
1
1
  import { simplifyChanges, ChangeSet } from 'prosemirror-changeset';
2
2
  import { Mark } from '@atlaskit/editor-prosemirror/model';
3
3
  import { Mapping, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
4
+ import { getAttributionKey } from '../decorations/colorSchemes/attributions';
4
5
  import { attrAwareTokenEncoder } from './attrAwareTokenEncoder';
5
6
  import { optimizeChanges } from './optimizeChanges';
6
7
  const WORD_CHAR_REGEX = /[\p{L}\p{N}_]/u;
@@ -177,9 +178,9 @@ const createMapping = maps => {
177
178
  }
178
179
  return mapping;
179
180
  };
180
- const createSpans = length => length > 0 ? [{
181
+ const createSpans = (length, data = null) => length > 0 ? [{
181
182
  length,
182
- data: null
183
+ data
183
184
  }] : [];
184
185
  const mergeOverlappingByNewDocRange = changes => {
185
186
  if (changes.length <= 1) {
@@ -191,8 +192,11 @@ const mergeOverlappingByNewDocRange = changes => {
191
192
  ...sortedChanges[0]
192
193
  };
193
194
  for (let i = 1; i < sortedChanges.length; i++) {
195
+ var _, _2;
194
196
  const next = sortedChanges[i];
195
- const isOverlapping = next.fromB <= current.toB;
197
+ const currentIdentity = getAttributionKey((_ = [...current.deleted, ...current.inserted][0]) === null || _ === void 0 ? void 0 : _.data);
198
+ const nextIdentity = getAttributionKey((_2 = [...next.deleted, ...next.inserted][0]) === null || _2 === void 0 ? void 0 : _2.data);
199
+ const isOverlapping = next.fromB <= current.toB && currentIdentity === nextIdentity;
196
200
  if (isOverlapping) {
197
201
  current = {
198
202
  fromA: Math.min(current.fromA, next.fromA),
@@ -248,12 +252,12 @@ const shouldCheckGranularDiff = (step, before, from, to) => {
248
252
  const isTextContentEqual = (replacedNode === null || replacedNode === void 0 ? void 0 : replacedNode.textContent) === (replacingNode === null || replacingNode === void 0 ? void 0 : replacingNode.textContent);
249
253
  return !isTextContentEqual || isTextContentEqual && hasSameChildMarks(replacedNode, replacingNode);
250
254
  };
251
- export const diffBySteps = (originalDoc, steps) => {
255
+ export const diffBySteps = (originalDoc, steps, stepAttributions) => {
252
256
  const changes = [];
253
257
  let currentDoc = originalDoc;
254
258
  const successfulStepMaps = [];
255
259
  const rangedSteps = [];
256
- for (const step of steps) {
260
+ for (const [stepIndex, step] of steps.entries()) {
257
261
  const before = currentDoc;
258
262
  const result = step.apply(currentDoc);
259
263
  if (result.failed !== null || !result.doc) {
@@ -268,6 +272,10 @@ export const diffBySteps = (originalDoc, steps) => {
268
272
  from: rangeStep.from,
269
273
  to: rangeStep.to,
270
274
  mapIndex: successfulStepMaps.length,
275
+ stepAttribution: stepAttributions !== null && stepAttributions !== void 0 && stepAttributions[stepIndex] ? {
276
+ ...stepAttributions[stepIndex],
277
+ stepIndex
278
+ } : null,
271
279
  step,
272
280
  stepMap
273
281
  });
@@ -343,8 +351,8 @@ export const diffBySteps = (originalDoc, steps) => {
343
351
  toA: granularToA,
344
352
  fromB: granularFromB,
345
353
  toB: granularToB,
346
- deleted: createSpans(Math.max(0, granularToA - granularFromA)),
347
- inserted: createSpans(Math.max(0, granularToB - granularFromB))
354
+ deleted: createSpans(Math.max(0, granularToA - granularFromA), rangedStep.stepAttribution),
355
+ inserted: createSpans(Math.max(0, granularToB - granularFromB), rangedStep.stepAttribution)
348
356
  });
349
357
  }
350
358
  continue;
@@ -354,8 +362,8 @@ export const diffBySteps = (originalDoc, steps) => {
354
362
  toA,
355
363
  fromB,
356
364
  toB,
357
- deleted: createSpans(Math.max(0, toA - fromA)),
358
- inserted: createSpans(Math.max(0, toB - fromB))
365
+ deleted: createSpans(Math.max(0, toA - fromA), rangedStep.stepAttribution),
366
+ inserted: createSpans(Math.max(0, toB - fromB), rangedStep.stepAttribution)
359
367
  });
360
368
  }
361
369
  return mergeOverlappingByNewDocRange(changes);
@@ -0,0 +1,64 @@
1
+ import { simplifyChanges } from 'prosemirror-changeset';
2
+ import { getAttributionKey } from '../decorations/colorSchemes/attributions';
3
+ import { optimizeChanges } from './optimizeChanges';
4
+ const getAttributionIdentity = change => {
5
+ const identities = new Set();
6
+ for (const span of [...change.deleted, ...change.inserted]) {
7
+ identities.add(getAttributionKey(span.data));
8
+ }
9
+ if (identities.size > 1) {
10
+ return {
11
+ key: undefined,
12
+ mergeable: false
13
+ };
14
+ }
15
+ return {
16
+ key: identities.values().next().value,
17
+ mergeable: true
18
+ };
19
+ };
20
+
21
+ /**
22
+ * Simplifies and optimizes consecutive changes without crossing an attribution boundary.
23
+ * A change which already contains more than one identity is deliberately kept isolated.
24
+ */
25
+ export const simplifyChangesWithAttribution = (changes, doc) => {
26
+ const result = [];
27
+ let run = [];
28
+ let runIdentity;
29
+ const flush = () => {
30
+ if (run.length > 0) {
31
+ // Some show-diff producers use structurally compatible Change objects rather than the
32
+ // library class. `simplifyChanges` reads runtime `lenA`/`lenB` getters even though they
33
+ // are intentionally omitted from its public declarations, so add equivalent getters
34
+ // without calling the library's internal constructor.
35
+ const normalizedRun = run.map(change => ({
36
+ ...change,
37
+ get lenA() {
38
+ return change.toA - change.fromA;
39
+ },
40
+ get lenB() {
41
+ return change.toB - change.fromB;
42
+ }
43
+ }));
44
+ result.push(...optimizeChanges(simplifyChanges(normalizedRun, doc)));
45
+ }
46
+ run = [];
47
+ runIdentity = undefined;
48
+ };
49
+ for (const change of changes) {
50
+ var _runIdentity;
51
+ const identity = getAttributionIdentity(change);
52
+ const canJoinRun = ((_runIdentity = runIdentity) === null || _runIdentity === void 0 ? void 0 : _runIdentity.mergeable) === true && identity.mergeable && runIdentity.key === identity.key;
53
+ if (run.length > 0 && !canJoinRun) {
54
+ flush();
55
+ }
56
+ run.push(change);
57
+ runIdentity = identity;
58
+ if (!identity.mergeable) {
59
+ flush();
60
+ }
61
+ }
62
+ flush();
63
+ return result;
64
+ };
@@ -1,7 +1,7 @@
1
1
  import { AnalyticsStep } from '@atlaskit/adf-schema/steps';
2
2
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
3
3
  import { ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
4
-
4
+ import { getAttributionKey } from '../decorations/colorSchemes/attributions';
5
5
  /**
6
6
  * Attempts to merge two consecutive ReplaceStep operations.
7
7
  * This merges steps where:
@@ -41,6 +41,69 @@ export function simplifySteps(steps, originalDoc) {
41
41
  return acc;
42
42
  }, []);
43
43
  }
44
+ const mergeAttributions = (first, second) => {
45
+ var _second$wasOffline;
46
+ if (!first) {
47
+ return second;
48
+ }
49
+ if (!second) {
50
+ return first;
51
+ }
52
+ return {
53
+ ...first,
54
+ ...second,
55
+ wasOffline: first.wasOffline === true || second.wasOffline === true ? true : (_second$wasOffline = second.wasOffline) !== null && _second$wasOffline !== void 0 ? _second$wasOffline : first.wasOffline
56
+ };
57
+ };
58
+
59
+ /**
60
+ * Attribution-preserving variant of `simplifySteps`.
61
+ *
62
+ * Steps may only merge when they have the same effective actor identity. Keeping the attribution
63
+ * coupled to the step also ensures filtering a no-op/analytics step cannot shift array indexes.
64
+ */
65
+ export function simplifyStepsWithAttribution(steps, stepAttributions, originalDoc) {
66
+ const stepsToFilter = removeUnusedAttributedSteps(steps.map((step, index) => ({
67
+ step,
68
+ stepAttribution: stepAttributions[index]
69
+ })), originalDoc);
70
+ return stepsToFilter.filter(({
71
+ step
72
+ }) => !(step instanceof AnalyticsStep)).reduce((acc, current) => {
73
+ var _previous$step$merge, _previous$step$merge2, _previous$step;
74
+ const previous = acc[acc.length - 1];
75
+ const isSameIdentity = previous && getAttributionKey(previous.stepAttribution) === getAttributionKey(current.stepAttribution);
76
+ const merged = isSameIdentity ? (_previous$step$merge = (_previous$step$merge2 = (_previous$step = previous.step).merge) === null || _previous$step$merge2 === void 0 ? void 0 : _previous$step$merge2.call(_previous$step, current.step)) !== null && _previous$step$merge !== void 0 ? _previous$step$merge : mergeReplaceSteps(previous.step, current.step) : null;
77
+ if (merged) {
78
+ acc[acc.length - 1] = {
79
+ step: merged,
80
+ stepAttribution: mergeAttributions(previous.stepAttribution, current.stepAttribution)
81
+ };
82
+ } else {
83
+ acc.push(current);
84
+ }
85
+ return acc;
86
+ }, []);
87
+ }
88
+
89
+ /**
90
+ * Does a first pass to remove steps that don't impact the document
91
+ */
92
+ function removeUnusedAttributedSteps(stepsWithAttribution, originalDoc) {
93
+ const finalSteps = [];
94
+ let firstPassDoc = originalDoc;
95
+ for (const stepWithAttribution of stepsWithAttribution) {
96
+ const {
97
+ step
98
+ } = stepWithAttribution;
99
+ const result = step.apply(firstPassDoc);
100
+ if (result.failed === null && result.doc && !areNodesEqualIgnoreAttrs(firstPassDoc, result.doc, ['localId'])) {
101
+ finalSteps.push(stepWithAttribution);
102
+ firstPassDoc = result.doc;
103
+ }
104
+ }
105
+ return finalSteps;
106
+ }
44
107
 
45
108
  /**
46
109
  * Does a first pass to remove steps that don't impact the document
@@ -0,0 +1,37 @@
1
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
2
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
3
+ const ATTRIBUTION_COLOR_PALETTE = ['purple', 'blue', 'teal', 'magenta', 'orange', 'green'];
4
+
5
+ /** `userId` is primary; `agentId`, or `agentType` as its fallback, separates shared users. */
6
+ export const getAttributionKey = attribution => {
7
+ var _attribution$userId, _attribution$agentId, _attribution$agentTyp;
8
+ const userId = attribution === null || attribution === void 0 ? void 0 : (_attribution$userId = attribution.userId) === null || _attribution$userId === void 0 ? void 0 : _attribution$userId.trim();
9
+ const agentId = attribution === null || attribution === void 0 ? void 0 : (_attribution$agentId = attribution.agentId) === null || _attribution$agentId === void 0 ? void 0 : _attribution$agentId.trim();
10
+ const agentType = attribution === null || attribution === void 0 ? void 0 : (_attribution$agentTyp = attribution.agentType) === null || _attribution$agentTyp === void 0 ? void 0 : _attribution$agentTyp.trim();
11
+ if (!userId && !agentId && !agentType) {
12
+ return undefined;
13
+ }
14
+ const agentIdentity = agentId || (agentType ? `type:${agentType}` : '');
15
+ return `user:${userId !== null && userId !== void 0 ? userId : ''}|agent:${agentIdentity}`;
16
+ };
17
+ export const areAttributionColorGatesEnabled = () => fg('confluence_ncs_step_diffing_version_history') && isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor');
18
+ export const isAttributionColoringEnabled = stepAttributions => areAttributionColorGatesEnabled() && (stepAttributions === null || stepAttributions === void 0 ? void 0 : stepAttributions.some(attribution => getAttributionKey(attribution) !== undefined)) === true;
19
+ export const createAttributionColorMap = stepAttributions => {
20
+ const keys = Array.from(new Set(stepAttributions.map(getAttributionKey).filter(key => key !== undefined))).sort();
21
+ return new Map(keys.map((key, index) => [key, ATTRIBUTION_COLOR_PALETTE[index % ATTRIBUTION_COLOR_PALETTE.length]]));
22
+ };
23
+ const getLatestAttribution = changes => {
24
+ const data = changes.flatMap(change => [...change.deleted, ...change.inserted]).map(span => span.data).filter(value => typeof value === 'object' && value !== null && 'stepIndex' in value && typeof value.stepIndex === 'number');
25
+ return data.reduce((latest, attribution) => !latest || attribution.stepIndex > latest.stepIndex ? attribution : latest, undefined);
26
+ };
27
+ const rangesOverlap = (fromA, toA, fromB, toB) => fromA < toB && fromB < toA;
28
+ const changesOverlap = (change, attributedChange) => {
29
+ const insertedOverlap = change.inserted.length > 0 && attributedChange.inserted.length > 0 && rangesOverlap(change.fromB, change.toB, attributedChange.fromB, attributedChange.toB);
30
+ const deletedOverlap = change.deleted.length > 0 && attributedChange.deleted.length > 0 && rangesOverlap(change.fromA, change.toA, attributedChange.fromA, attributedChange.toA);
31
+ return insertedOverlap || deletedOverlap;
32
+ };
33
+ export const getColorSchemeForChange = (change, attributedChanges, attributionColors, fallback) => {
34
+ var _attributionColors$ge;
35
+ const key = getAttributionKey(getLatestAttribution(attributedChanges.filter(attributedChange => changesOverlap(change, attributedChange))));
36
+ return key ? (_attributionColors$ge = attributionColors.get(key)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : fallback : fallback;
37
+ };
@@ -52,9 +52,26 @@ export const standardScheme = {
52
52
  deletedQuoteNodeActiveTone: 'borderAccent',
53
53
  strikesDeletedEmbedCard: false
54
54
  };
55
+ const createAttributionScheme = color => ({
56
+ ...standardScheme,
57
+ insertColor: color,
58
+ insertActiveColor: color,
59
+ deleteColor: color,
60
+ deleteActiveColor: color,
61
+ deletedCellColor: color
62
+ });
55
63
 
56
- /** Maps the public ColorScheme string to its data object. Adding a scheme = one entry here. */
64
+ /** Maps public schemes and attribution-only accents to their data objects. */
57
65
  export const colorSchemeRegistry = {
66
+ blue: createAttributionScheme('blue'),
67
+ green: createAttributionScheme('green'),
68
+ magenta: createAttributionScheme('magenta'),
69
+ orange: createAttributionScheme('orange'),
70
+ purple: createAttributionScheme('purple'),
71
+ teal: createAttributionScheme('teal'),
58
72
  traditional: traditionalScheme,
59
73
  standard: standardScheme
60
- };
74
+ };
75
+
76
+ /** Attribution accents are impossible in the refactor-off cohort; fall back defensively. */
77
+ export const getLegacyColorScheme = colorScheme => colorScheme === 'standard' || colorScheme === 'traditional' ? colorScheme : undefined;
@@ -4,7 +4,7 @@ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-e
4
4
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
5
  import { isExtendedEnabled } from '../isExtendedEnabled';
6
6
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedBlockNodeStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildInsertedBlockNodeStyle } from './colorSchemes/factory';
7
- import { colorSchemeRegistry } from './colorSchemes/schemes';
7
+ import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
8
8
  import { createBlockIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
9
9
  import { buildDiffDecorationSpec } from './decorationKeys';
10
10
  import { getBlockNodeStyleLegacy, resolveCellOverlayStyleLegacy } from './createBlockChangedDecoration.styles.legacy';
@@ -95,7 +95,10 @@ const getBlockNodeStyleNext = ({
95
95
  * OFF cohort of `platform_editor_show_diff_color_scheme_refactor`. Both are built to emit the
96
96
  * same style strings for both shipped schemes; see EDITOR-8281.
97
97
  */
98
- const getBlockNodeStyle = props => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? getBlockNodeStyleNext(props) : getBlockNodeStyleLegacy(props);
98
+ const getBlockNodeStyle = props => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? getBlockNodeStyleNext(props) : getBlockNodeStyleLegacy({
99
+ ...props,
100
+ colorScheme: getLegacyColorScheme(props.colorScheme)
101
+ });
99
102
 
100
103
  /**
101
104
  * A table cell is "empty" only when it has no content at all — no text AND no
@@ -165,7 +168,7 @@ export const createBlockChangedDecoration = ({
165
168
  const isEmptyCellBeingFilled = !isInserted && !!cellNode && isCellEmpty(cellNode);
166
169
  const useAddedStyle = isInserted || isEmptyCellBeingFilled;
167
170
  const cellOverlayStyle = isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? useAddedStyle ? isRoundedTable ? buildAddedCellOverlayRoundedStyle(colors) : buildAddedCellOverlayStyle(colors) : isRoundedTable ? buildDeletedCellOverlayRoundedStyle(colors) : buildDeletedCellOverlayStyle(colors) : resolveCellOverlayStyleLegacy({
168
- colorScheme,
171
+ colorScheme: getLegacyColorScheme(colorScheme),
169
172
  isRoundedTable,
170
173
  useAddedStyle
171
174
  });
@@ -6,7 +6,7 @@ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-e
6
6
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
7
7
  import { isExtendedEnabled } from '../isExtendedEnabled';
8
8
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildDeletedRowStyle } from './colorSchemes/factory';
9
- import { colorSchemeRegistry } from './colorSchemes/schemes';
9
+ import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
10
10
  import { resolveCellOverlayStyleLegacy, resolveDeletedRowStyleLegacy } from './createChangedRowDecorationWidgets.styles.legacy';
11
11
  import { buildDiffDecorationSpec } from './decorationKeys';
12
12
  import { findSafeInsertPos } from './utils/findSafeInsertPos';
@@ -120,7 +120,7 @@ const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorSc
120
120
 
121
121
  // Inserted rows keep their natural styling; the row strikethrough is deletions only.
122
122
  if (!isExtendedEnabled(diffType) || !isInserted) {
123
- tr.setAttribute('style', isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(colorScheme));
123
+ tr.setAttribute('style', isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? buildDeletedRowStyle(colors) : resolveDeletedRowStyleLegacy(getLegacyColorScheme(colorScheme)));
124
124
  }
125
125
  tr.setAttribute('data-testid', 'show-diff-deleted-row');
126
126
 
@@ -136,7 +136,7 @@ const createChangedRowDOM = (rowNode, cellEdgeAttrs, nodeViewSerializer, colorSc
136
136
  const overlay = document.createElement('span');
137
137
  const isRoundedTable = expValEquals('platform_editor_table_diff_rounded_corners', 'isEnabled', true);
138
138
  const overlayStyle = isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? isInserted ? isRoundedTable ? buildAddedCellOverlayRoundedStyle(colors) : buildAddedCellOverlayStyle(colors) : isRoundedTable ? buildDeletedCellOverlayRoundedStyle(colors) : buildDeletedCellOverlayStyle(colors) : resolveCellOverlayStyleLegacy({
139
- colorScheme,
139
+ colorScheme: getLegacyColorScheme(colorScheme),
140
140
  isInserted,
141
141
  isRoundedTable
142
142
  });
@@ -3,7 +3,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
3
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
4
  import { isExtendedEnabled } from '../isExtendedEnabled';
5
5
  import { buildAtomicInlineChangedCSSVariables, buildDeletedInlineContentStyleExtended, buildDeletedInlineStyle, buildDeletedInlineStyleStandard, buildInsertStyle, buildInsertStyleActive, buildInsertStyleExtended, buildInsertStyleExtendedActive, buildInsertStyleExtendedNoUnderline, buildInsertStyleExtendedNoUnderlineActive } from './colorSchemes/factory';
6
- import { colorSchemeRegistry } from './colorSchemes/schemes';
6
+ import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
7
7
  import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
8
8
  import { getAtomicInlineNodeClassNameLegacy, resolveInlineChangedStyleLegacy } from './createInlineChangedDecoration.styles.legacy';
9
9
  import { buildDiffDecorationSpec } from './decorationKeys';
@@ -64,7 +64,10 @@ const resolveInlineChangedStyleRefactored = ({
64
64
  * Single gate for the inline `style` string: registry + factory when the refactor is on, the
65
65
  * verbatim pre-refactor per-scheme constants when it is off.
66
66
  */
67
- const resolveInlineChangedStyle = args => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? resolveInlineChangedStyleRefactored(args) : resolveInlineChangedStyleLegacy(args);
67
+ const resolveInlineChangedStyle = args => isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor') ? resolveInlineChangedStyleRefactored(args) : resolveInlineChangedStyleLegacy({
68
+ ...args,
69
+ colorScheme: getLegacyColorScheme(args.colorScheme)
70
+ });
68
71
 
69
72
  /**
70
73
  * Single gate for the atomic-inline-node decoration attributes. The two cohorts differ in the DOM:
@@ -76,7 +79,7 @@ const resolveAtomicInlineAttrs = (inlineNodeName, colorScheme, colors) => isExpe
76
79
  className: getAtomicInlineNodeClassName(inlineNodeName),
77
80
  styleSuffix: buildAtomicInlineChangedCSSVariables(colors)
78
81
  } : {
79
- className: getAtomicInlineNodeClassNameLegacy(inlineNodeName, colorScheme),
82
+ className: getAtomicInlineNodeClassNameLegacy(inlineNodeName, getLegacyColorScheme(colorScheme)),
80
83
  styleSuffix: ''
81
84
  };
82
85