@atlaskit/editor-plugin-show-diff 14.3.2 → 14.4.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 (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/afm-cc/tsconfig.json +18 -0
  3. package/afm-products/tsconfig.json +18 -0
  4. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +47 -26
  5. package/dist/cjs/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +61 -9
  6. package/dist/cjs/pm-plugins/decorations/colorSchemes/attributions.js +77 -20
  7. package/dist/cjs/pm-plugins/decorations/colorSchemes/factory.js +10 -0
  8. package/dist/cjs/pm-plugins/decorations/colorSchemes/types.js +2 -0
  9. package/dist/cjs/pm-plugins/decorations/createAnchorDecorationWidgets.js +52 -7
  10. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +172 -0
  11. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +37 -5
  12. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +40 -7
  13. package/dist/cjs/pm-plugins/decorations/decorationKeys.js +31 -5
  14. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +128 -0
  15. package/dist/cjs/pm-plugins/main.js +57 -6
  16. package/dist/cjs/pm-plugins/resolveDiffContributors.js +143 -0
  17. package/dist/cjs/showDiffPlugin.js +21 -6
  18. package/dist/cjs/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  19. package/dist/cjs/ui/ContributorTag/ContributorTag.js +383 -0
  20. package/dist/cjs/ui/ContributorTag/ContributorTagWidget.js +50 -0
  21. package/dist/cjs/ui/ContributorTag/messages.js +24 -0
  22. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +48 -27
  23. package/dist/es2019/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +39 -0
  24. package/dist/es2019/pm-plugins/decorations/colorSchemes/attributions.js +53 -11
  25. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +9 -0
  26. package/dist/es2019/pm-plugins/decorations/colorSchemes/types.js +2 -0
  27. package/dist/es2019/pm-plugins/decorations/createAnchorDecorationWidgets.js +43 -1
  28. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +163 -0
  29. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +38 -5
  30. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +38 -6
  31. package/dist/es2019/pm-plugins/decorations/decorationKeys.js +30 -2
  32. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +101 -0
  33. package/dist/es2019/pm-plugins/main.js +52 -3
  34. package/dist/es2019/pm-plugins/resolveDiffContributors.js +128 -0
  35. package/dist/es2019/showDiffPlugin.js +20 -5
  36. package/dist/es2019/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  37. package/dist/es2019/ui/ContributorTag/ContributorTag.js +312 -0
  38. package/dist/es2019/ui/ContributorTag/ContributorTagWidget.js +43 -0
  39. package/dist/es2019/ui/ContributorTag/messages.js +18 -0
  40. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +49 -28
  41. package/dist/esm/pm-plugins/calculateDiff/simplifyChangesWithAttribution.js +60 -8
  42. package/dist/esm/pm-plugins/decorations/colorSchemes/attributions.js +77 -19
  43. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +9 -0
  44. package/dist/esm/pm-plugins/decorations/colorSchemes/types.js +2 -0
  45. package/dist/esm/pm-plugins/decorations/createAnchorDecorationWidgets.js +51 -6
  46. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +166 -0
  47. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +37 -5
  48. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +40 -7
  49. package/dist/esm/pm-plugins/decorations/decorationKeys.js +30 -5
  50. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +122 -0
  51. package/dist/esm/pm-plugins/main.js +57 -6
  52. package/dist/esm/pm-plugins/resolveDiffContributors.js +137 -0
  53. package/dist/esm/showDiffPlugin.js +21 -6
  54. package/dist/esm/ui/ContributorTag/ContributorTag.compiled.css +29 -0
  55. package/dist/esm/ui/ContributorTag/ContributorTag.js +375 -0
  56. package/dist/esm/ui/ContributorTag/ContributorTagWidget.js +43 -0
  57. package/dist/esm/ui/ContributorTag/messages.js +18 -0
  58. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  59. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +7 -1
  60. package/dist/types/pm-plugins/calculateDiff/simplifyChangesWithAttribution.d.ts +10 -0
  61. package/dist/types/pm-plugins/decorations/colorSchemes/attributions.d.ts +24 -2
  62. package/dist/types/pm-plugins/decorations/colorSchemes/factory.d.ts +7 -1
  63. package/dist/types/pm-plugins/decorations/colorSchemes/schemes.d.ts +1 -2
  64. package/dist/types/pm-plugins/decorations/colorSchemes/types.d.ts +2 -1
  65. package/dist/types/pm-plugins/decorations/createAnchorDecorationWidgets.d.ts +14 -0
  66. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +55 -0
  67. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +5 -1
  68. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +5 -1
  69. package/dist/types/pm-plugins/decorations/decorationKeys.d.ts +28 -3
  70. package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +9 -0
  71. package/dist/types/pm-plugins/main.d.ts +8 -2
  72. package/dist/types/pm-plugins/resolveDiffContributors.d.ts +7 -0
  73. package/dist/types/showDiffPluginType.d.ts +65 -2
  74. package/dist/types/ui/ContributorTag/ContributorTag.d.ts +37 -0
  75. package/dist/types/ui/ContributorTag/ContributorTagWidget.d.ts +22 -0
  76. package/dist/types/ui/ContributorTag/messages.d.ts +6 -0
  77. package/package.json +16 -7
@@ -12,8 +12,9 @@ import { createDocMarginAnchorWidget } from '../decorations/createAnchorDecorati
12
12
  import { createBlockChangedDecoration } from '../decorations/createBlockChangedDecoration';
13
13
  import { createInlineChangedDecoration } from '../decorations/createInlineChangedDecoration';
14
14
  import { createNodeChangedDecorationWidget } from '../decorations/createNodeChangedDecorationWidget';
15
- import { createAttributionColorMap, getAttributionKey, getColorSchemeForChange, isAttributionColoringEnabled } from '../decorations/colorSchemes/attributions';
15
+ import { createAttributionColorMap, getAttributionKey, getAttributionKeyForChange, getColorSchemeForChange, isAttributionColoringEnabled, isContributorTagsEnabled } from '../decorations/colorSchemes/attributions';
16
16
  import { extractDiffDescriptors } from '../decorations/decorationKeys';
17
+ import { extractContributorTags } from '../decorations/extractContributorTags';
17
18
  import { getAttrChangeRanges, stepIsValidAttrChange } from '../decorations/utils/getAttrChangeRanges';
18
19
  import { getMarkChangeRanges } from '../decorations/utils/getMarkChangeRanges';
19
20
  import { getDefaultDiffType, isExtendedEnabled } from '../isExtendedEnabled';
@@ -22,7 +23,7 @@ import { groupChangesByBlock } from './groupChangesByBlock';
22
23
  import { isMarkOnlyChange } from './isMarkOnlyChange';
23
24
  import { optimizeChanges } from './optimizeChanges';
24
25
  import { selectTokenEncoder } from './selectTokenEncoder';
25
- import { simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
26
+ import { collapseOverlappingChanges, simplifyChangesWithAttribution } from './simplifyChangesWithAttribution';
26
27
  import { simplifySteps, simplifyStepsWithAttribution } from './simplifySteps';
27
28
  import { classifySmartChanges } from './smart/classifySmartChanges';
28
29
  import { smartChangeLevel } from './smart/helpers';
@@ -68,20 +69,13 @@ const getAttributedChanges = ({
68
69
  originalDoc,
69
70
  steppedDoc,
70
71
  diffType,
71
- tr,
72
- intl,
73
- smartThresholds
72
+ tr
74
73
  }) => {
74
+ // `smart` is deliberately absent here, even though `getChanges` applies it: the classifier drops
75
+ // the step attribution and promotes ranges to sentence/paragraph/node granularity, so a promoted
76
+ // range covers several actors' steps and the latest-writer rule credits all of it to one of them.
77
+ // The attributed changeset keeps step granularity instead.
75
78
  if (isExtendedEnabled(diffType)) {
76
- if (diffType === 'smart' && fg('platform_editor_ai_smart_diff')) {
77
- return classifySmartChanges({
78
- changes: simplifyChangesWithAttribution(attributedChanges, tr.doc),
79
- originalDoc,
80
- newDoc: tr.doc,
81
- locale: intl.locale,
82
- thresholds: smartThresholds
83
- });
84
- }
85
79
  if (diffType === 'step') {
86
80
  return attributedChanges;
87
81
  }
@@ -248,16 +242,19 @@ const calculateDiffDecorationsInner = ({
248
242
  showIndicators = false,
249
243
  smartThresholds,
250
244
  deletedDiffPlacement = 'top',
251
- inlineDeletedDiffPlacement = 'before'
245
+ inlineDeletedDiffPlacement = 'before',
246
+ tagMountContext
252
247
  }) => {
253
248
  const {
254
249
  originalDoc,
255
250
  steps,
256
251
  stepAttributions,
252
+ contributors,
257
253
  isDisplayingChanges
258
254
  } = pluginState;
259
255
  if (!originalDoc || !isDisplayingChanges) {
260
256
  return {
257
+ contributorTags: [],
261
258
  decorations: DecorationSet.empty,
262
259
  diffDescriptors: []
263
260
  };
@@ -271,9 +268,12 @@ const calculateDiffDecorationsInner = ({
271
268
  } = state;
272
269
  let steppedDoc = originalDoc;
273
270
  const attributionColoringEnabled = isAttributionColoringEnabled(stepAttributions);
271
+ const showContributorTags = isContributorTagsEnabled(stepAttributions, contributors);
272
+ // Tags need the attributed changeset even on diffs with too few actors to be worth colouring.
273
+ const attributedChangesetEnabled = attributionColoringEnabled || showContributorTags;
274
274
  let simplifiedSteps;
275
275
  let simplifiedStepAttributions = [];
276
- if (attributionColoringEnabled) {
276
+ if (attributedChangesetEnabled) {
277
277
  const simplifiedStepsWithAttribution = simplifyStepsWithAttribution(steps, stepAttributions !== null && stepAttributions !== void 0 ? stepAttributions : [], originalDoc);
278
278
  simplifiedSteps = simplifiedStepsWithAttribution.map(({
279
279
  step
@@ -350,6 +350,7 @@ const calculateDiffDecorationsInner = ({
350
350
  });
351
351
  if (!recoveredViaContentEquality) {
352
352
  return {
353
+ contributorTags: [],
353
354
  decorations: DecorationSet.empty,
354
355
  diffDescriptors: []
355
356
  };
@@ -363,7 +364,7 @@ const calculateDiffDecorationsInner = ({
363
364
  let changes;
364
365
  let attributedChanges = [];
365
366
  let attributionColors;
366
- if (attributionColoringEnabled) {
367
+ if (attributedChangesetEnabled) {
367
368
  const changeset = ChangeSet.create(originalDoc, (left, right) => getAttributionKey(left) === getAttributionKey(right) ? left.stepIndex > right.stepIndex ? left : right :
368
369
  // `prosemirror-changeset` uses null as its incompatible-data sentinel even
369
370
  // though its public combine callback is typed as returning Data.
@@ -372,17 +373,22 @@ const calculateDiffDecorationsInner = ({
372
373
  stepIndex
373
374
  })));
374
375
  attributedChanges = diffBySteps(originalDoc, simplifiedSteps, simplifiedStepAttributions);
375
- changes = getAttributedChanges({
376
+ // The attributed pipeline gives no guarantee that its output ranges are disjoint — see
377
+ // `collapseOverlappingChanges`.
378
+ const uncollapsedChanges = getAttributedChanges({
376
379
  changeset,
377
380
  originalDoc,
378
381
  steppedDoc,
379
382
  diffType,
380
383
  tr,
381
- attributedChanges,
382
- intl,
383
- smartThresholds
384
+ attributedChanges
384
385
  });
385
- attributionColors = createAttributionColorMap(simplifiedStepAttributions);
386
+ changes = collapseOverlappingChanges(uncollapsedChanges);
387
+
388
+ // Still colours-only: a tags-only diff keeps the plain colour scheme.
389
+ if (attributionColoringEnabled) {
390
+ attributionColors = createAttributionColorMap(simplifiedStepAttributions);
391
+ }
386
392
  } else {
387
393
  const changeset = ChangeSet.create(originalDoc, undefined, tokenEncoder).addSteps(steppedDoc, stepMaps, tr.doc);
388
394
  changes = getChanges({
@@ -409,6 +415,7 @@ const calculateDiffDecorationsInner = ({
409
415
  const isInserted = !isInverted;
410
416
  const createDecorationsForChange = change => {
411
417
  const changeColorScheme = attributionColors ? getColorSchemeForChange(change, attributedChanges, attributionColors, colorScheme) : colorScheme;
418
+ const attributionKey = showContributorTags ? getAttributionKeyForChange(change, attributedChanges) : undefined;
412
419
  const isActive = isRangeActive(activeIndexPos, change.fromB, change.toB);
413
420
 
414
421
  // The deleted side of a mark-only change is a byte-identical copy of the new text, so it is
@@ -462,6 +469,7 @@ const calculateDiffDecorationsInner = ({
462
469
  continue;
463
470
  }
464
471
  decorations.push(...createInlineChangedDecoration({
472
+ attributionKey,
465
473
  change: {
466
474
  fromB: from,
467
475
  toB: to
@@ -473,15 +481,18 @@ const calculateDiffDecorationsInner = ({
473
481
  ...(isExtendedEnabled(diffType) && {
474
482
  isInserted,
475
483
  shouldHideDeleted,
484
+ showContributorTags,
476
485
  showIndicators,
477
486
  hideAddedDiffsUnderline,
478
487
  hasDeletedWidget: willRenderDeletedWidget,
479
- isDeletedWidgetBelow
488
+ isDeletedWidgetBelow,
489
+ tagMountContext
480
490
  })
481
491
  }));
482
492
  }
483
493
  } else {
484
494
  decorations.push(...createInlineChangedDecoration({
495
+ attributionKey,
485
496
  change,
486
497
  doc: tr.doc,
487
498
  colorScheme: changeColorScheme,
@@ -490,10 +501,12 @@ const calculateDiffDecorationsInner = ({
490
501
  ...(isExtendedEnabled(diffType) && {
491
502
  isInserted,
492
503
  shouldHideDeleted,
504
+ showContributorTags,
493
505
  showIndicators,
494
506
  hideAddedDiffsUnderline,
495
507
  hasDeletedWidget: willRenderDeletedWidget,
496
- isDeletedWidgetBelow
508
+ isDeletedWidgetBelow,
509
+ tagMountContext
497
510
  })
498
511
  }));
499
512
  }
@@ -517,6 +530,8 @@ const calculateDiffDecorationsInner = ({
517
530
  const shouldHideDeleted = isExtendedEnabled(diffType) ? !isInverted && hideDeletedDiffs && change.inserted.length > 0 : false;
518
531
  if (!shouldHideDeleted) {
519
532
  decorations.push(...createNodeChangedDecorationWidget({
533
+ // The deleted side has no inline decoration, so the widget carries the actor.
534
+ attributionKey,
520
535
  change,
521
536
  doc: originalDoc,
522
537
  nodeViewSerializer,
@@ -524,6 +539,8 @@ const calculateDiffDecorationsInner = ({
524
539
  newDoc: tr.doc,
525
540
  intl,
526
541
  activeIndexPos,
542
+ showContributorTags,
543
+ tagMountContext,
527
544
  ...(isExtendedEnabled(diffType) && {
528
545
  isInserted: !isInserted,
529
546
  diffType,
@@ -635,12 +652,14 @@ const calculateDiffDecorationsInner = ({
635
652
  });
636
653
  const decorationSet = DecorationSet.empty.add(tr.doc, decorations);
637
654
  return {
655
+ contributorTags: showContributorTags ? extractContributorTags(decorationSet, contributors) : [],
638
656
  decorations: decorationSet,
639
657
  diffDescriptors: extractDiffDescriptors(decorationSet)
640
658
  };
641
659
  };
642
660
  export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner,
643
- // Cache results unless relevant inputs change
661
+ // Cache results unless relevant inputs change. `tagMountContext` is deliberately absent: it is
662
+ // built once per plugin instance, so it can never be the reason a diff needs recalculating.
644
663
  ([{
645
664
  pluginState,
646
665
  state,
@@ -674,7 +693,9 @@ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner
674
693
  const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
675
694
  if (isExtendedEnabled(diffType)) {
676
695
  var _ref;
677
- 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;
696
+ 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) &&
697
+ // Contributors can arrive after the first calculation; without this, tags never appear.
698
+ isEqual(pluginState.contributors, lastPluginState.contributors) && state.doc.eq(lastState.doc) && hideDeletedDiffs === lastHideDeletedDiffs && hideAddedDiffsUnderline === lastHideAddedDiffsUnderline && showIndicators === lastShowIndicators && isEqual(smartThresholds, lastSmartThresholds) && deletedDiffPlacement === lastDeletedDiffPlacement && inlineDeletedDiffPlacement === lastInlineDeletedDiffPlacement) !== null && _ref !== void 0 ? _ref : false;
678
699
  }
679
- 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;
700
+ return (_ref2 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && isEqual(pluginState.stepAttributions, lastPluginState.stepAttributions) && isEqual(pluginState.contributors, lastPluginState.contributors) && state.doc.eq(lastState.doc) && colorScheme === lastColorScheme && intl.locale === lastIntl.locale && isEqual(activeIndexPos, lastActiveIndexPos) && hideDeletedDiffs === lastHideDeletedDiffs) !== null && _ref2 !== void 0 ? _ref2 : false;
680
701
  });
@@ -17,6 +17,45 @@ const getAttributionIdentity = change => {
17
17
  mergeable: true
18
18
  };
19
19
  };
20
+ const byNewDocRange = (left, right) => left.fromB - right.fromB || left.toB - right.toB;
21
+
22
+ /**
23
+ * Collapses changes whose new-document ranges strictly overlap. Attribution runs are simplified
24
+ * independently, so runs either side of an unattributed step can come back covering the same
25
+ * characters — which stacks two inline decorations and renders one contributor tag per copy.
26
+ *
27
+ * Merging keeps every span from both sides, so the latest-writer rule still decides whose tag is
28
+ * shown. Ranges that merely touch are left alone, so an unattributed edit next to an attributed one
29
+ * is not credited to it.
30
+ */
31
+ export const collapseOverlappingChanges = changes => {
32
+ if (changes.length <= 1) {
33
+ return changes;
34
+ }
35
+
36
+ // A zero-length new-document range paints no inline decoration, so it is kept aside rather than
37
+ // folded into a neighbouring insertion.
38
+ const emptyRanges = changes.filter(change => change.toB <= change.fromB);
39
+ const collapsed = [];
40
+ for (const change of changes.filter(change => change.toB > change.fromB).sort(byNewDocRange)) {
41
+ const current = collapsed[collapsed.length - 1];
42
+ if (current && change.fromB < current.toB) {
43
+ collapsed[collapsed.length - 1] = {
44
+ fromA: Math.min(current.fromA, change.fromA),
45
+ toA: Math.max(current.toA, change.toA),
46
+ fromB: Math.min(current.fromB, change.fromB),
47
+ toB: Math.max(current.toB, change.toB),
48
+ deleted: [...current.deleted, ...change.deleted],
49
+ inserted: [...current.inserted, ...change.inserted]
50
+ };
51
+ } else {
52
+ collapsed.push({
53
+ ...change
54
+ });
55
+ }
56
+ }
57
+ return [...collapsed, ...emptyRanges].sort(byNewDocRange);
58
+ };
20
59
 
21
60
  /**
22
61
  * Simplifies and optimizes consecutive changes without crossing an attribution boundary.
@@ -2,6 +2,11 @@ import { getParticipantColor } from '@atlaskit/editor-shared-styles/utils';
2
2
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
3
  import { fg } from '@atlaskit/platform-feature-flags/fg';
4
4
  import { PARTICIPANT_COLOR_SCHEMES } from './types';
5
+
6
+ /** A contributor with both of its identities collapsed to internal attribution keys. */
7
+
8
+ /** Contributors addressed by attribution key. The key format is internal. */
9
+
5
10
  /**
6
11
  * Normalise the attribution identity once so a valid lookup key always has a valid participant
7
12
  * colour seed. `userId` is primary; `agentId`, or `agentType` as its fallback, separates shared
@@ -36,6 +41,35 @@ export const getAttributionKey = attribution => {
36
41
  var _getAttributionIdenti;
37
42
  return (_getAttributionIdenti = getAttributionIdentity(attribution)) === null || _getAttributionIdenti === void 0 ? void 0 : _getAttributionIdenti.key;
38
43
  };
44
+
45
+ /**
46
+ * Public contributor list to internal key-addressed record. A contributor whose attribution carries
47
+ * no identity is dropped, since it could never match a step. Later entries win.
48
+ */
49
+ export const resolveContributors = contributors => {
50
+ if (!(contributors !== null && contributors !== void 0 && contributors.length)) {
51
+ return undefined;
52
+ }
53
+ const resolved = {};
54
+ for (const {
55
+ attribution,
56
+ connectedTo,
57
+ ...tagContributor
58
+ } of contributors) {
59
+ const key = getAttributionKey(attribution);
60
+ if (!key) {
61
+ continue;
62
+ }
63
+ const connectedToKey = connectedTo ? getAttributionKey(connectedTo) : undefined;
64
+ resolved[key] = {
65
+ ...tagContributor,
66
+ ...(connectedToKey ? {
67
+ connectedToKey
68
+ } : {})
69
+ };
70
+ }
71
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
72
+ };
39
73
  export const areAttributionColorGatesEnabled = () => fg('confluence_ncs_step_diffing_version_history') && isExperimentEnabled('platform_editor_show_diff_color_scheme_refactor');
40
74
  export const isAttributionColoringEnabled = stepAttributions => {
41
75
  if (!areAttributionColorGatesEnabled() || !(stepAttributions !== null && stepAttributions !== void 0 && stepAttributions.length)) {
@@ -44,6 +78,12 @@ export const isAttributionColoringEnabled = stepAttributions => {
44
78
  const attributionKeys = new Set(stepAttributions.map(getAttributionKey).filter(key => key !== undefined));
45
79
  return attributionKeys.size > 1;
46
80
  };
81
+
82
+ /**
83
+ * Shares the colours' gates, but with a lower bar: colouring needs two actors to tell apart, whereas
84
+ * a tag only has to name one.
85
+ */
86
+ export const isContributorTagsEnabled = (stepAttributions, contributors) => areAttributionColorGatesEnabled() && Boolean(stepAttributions === null || stepAttributions === void 0 ? void 0 : stepAttributions.some(attribution => getAttributionKey(attribution) !== undefined)) && contributors !== undefined && Object.keys(contributors).length > 0;
47
87
  export const createAttributionColorMap = stepAttributions => {
48
88
  const colors = new Map();
49
89
  for (const attribution of stepAttributions) {
@@ -71,17 +111,19 @@ const changesOverlap = (change, attributedChange) => {
71
111
  const deletedOverlap = change.deleted.length > 0 && attributedChange.deleted.length > 0 && rangesOverlap(change.fromA, change.toA, attributedChange.fromA, attributedChange.toA);
72
112
  return insertedOverlap || deletedOverlap;
73
113
  };
74
- const getParticipantColorSchemeForChanges = (changes, attributionColors) => {
75
- const key = getAttributionKey(getLatestAttribution(changes));
76
- return key ? attributionColors.get(key) : undefined;
114
+ const getAttributionKeyForChanges = changes => getAttributionKey(getLatestAttribution(changes));
115
+
116
+ /**
117
+ * The attribution key owning a change. Inline and step changes retain direct attribution spans, so
118
+ * resolve those first. Grouped and smart-classified changes can lose that data; for those, fall
119
+ * back to the latest overlapping attributed step.
120
+ */
121
+ export const getAttributionKeyForChange = (change, attributedChanges) => {
122
+ var _getAttributionKeyFor;
123
+ return (_getAttributionKeyFor = getAttributionKeyForChanges([change])) !== null && _getAttributionKeyFor !== void 0 ? _getAttributionKeyFor : getAttributionKeyForChanges(attributedChanges.filter(attributedChange => changesOverlap(change, attributedChange)));
77
124
  };
78
125
  export const getColorSchemeForChange = (change, attributedChanges, attributionColors, fallback) => {
79
- var _ref, _getParticipantColorS;
80
- return (// Inline and step changes retain their own attribution spans. Resolve those first so a broader
81
- // change is not recoloured by a later contributor whose nested range happens to overlap it.
82
- (_ref = (_getParticipantColorS = getParticipantColorSchemeForChanges([change], attributionColors)) !== null && _getParticipantColorS !== void 0 ? _getParticipantColorS :
83
- // Grouping and smart classification can produce changes without direct attribution data. For
84
- // those transformed changes, retain the overlap-based lookup as a fallback.
85
- getParticipantColorSchemeForChanges(attributedChanges.filter(attributedChange => changesOverlap(change, attributedChange)), attributionColors)) !== null && _ref !== void 0 ? _ref : fallback
86
- );
126
+ var _attributionColors$ge;
127
+ const key = getAttributionKeyForChange(change, attributedChanges);
128
+ return key ? (_attributionColors$ge = attributionColors.get(key)) !== null && _attributionColors$ge !== void 0 ? _attributionColors$ge : fallback : fallback;
87
129
  };
@@ -142,6 +142,15 @@ function textAccent(color) {
142
142
  return isParticipantColorScheme(color) ? participantColorTokenSets[color].textAccent : textAccentMap[color];
143
143
  }
144
144
 
145
+ /** Presentation tokens for one diff colour, shared by highlights and contributor tags. */
146
+ export function getAccentTokens(color) {
147
+ return {
148
+ background: bgSubtlest(color),
149
+ border: borderAccent(color),
150
+ text: textAccent(color)
151
+ };
152
+ }
153
+
145
154
  /** Ring colour for an active (scroll-target) deleted block outline. */
146
155
  function deletedBlockOutlineActiveRing(colors) {
147
156
  return colors.deletedBlockOutlineActiveEmphasis === 'border' ? borderAccent(colors.deleteActiveColor) : bgSubtlerPressed(colors.deleteActiveColor);
@@ -1,3 +1,5 @@
1
+ /** Schemes consumers may explicitly request through the show-diff API. */
2
+
1
3
  /** Attribution-only participant slots selected by the plugin, never by consumers. */
2
4
  export const PARTICIPANT_COLOR_SCHEMES = ['red-bolder', 'blue-bolder', 'green-bolder', 'yellow-bolder', 'purple-bolder', 'magenta-bolder', 'teal-bolder', 'orange-bolder', 'lime-bolder', 'gray-bolder', 'blue-subtle', 'red-subtle', 'orange-subtle', 'yellow-subtle', 'green-subtle', 'teal-subtle', 'purple-subtle', 'magenta-subtle'];
3
5
 
@@ -1,5 +1,6 @@
1
1
  import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
2
2
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
3
4
  import { buildAnchorDecorationKey, buildAnchorDecorationSpec, AnchorDocMarginKey, AnchorTypeKey } from './decorationKeys';
4
5
 
5
6
  /**
@@ -315,7 +316,7 @@ export const createBlockIndicatorAnchorWidgets = ({
315
316
  * inward into the neighbouring cell (`direction: 1` forward, `-1` back). Positions
316
317
  * not on a row boundary are returned as-is.
317
318
  */
318
- const clampAnchorPosIntoCell = (doc, pos, direction) => {
319
+ export const clampAnchorPosIntoCell = (doc, pos, direction) => {
319
320
  const $pos = doc.resolve(pos);
320
321
  if ($pos.parent.type.name !== 'tableRow') {
321
322
  return pos;
@@ -329,6 +330,31 @@ const clampAnchorPosIntoCell = (doc, pos, direction) => {
329
330
  return direction === 1 ? pos + 2 : pos - 2;
330
331
  };
331
332
 
333
+ /**
334
+ * A zero-size inline span carrying nothing but the `anchor-name` an overlay aligns against. A
335
+ * fragmented inline element reports the union of its line fragments, so aligning against one
336
+ * horizontally gives the enclosing block's content edge; a zero-size span is always a single
337
+ * fragment.
338
+ */
339
+ export const createAnchorNameSpan = anchorKey => {
340
+ const span = document.createElement('span');
341
+ span.style.setProperty('anchor-name', `--${anchorKey}`);
342
+ return span;
343
+ };
344
+ const createAnchorSpanWidget = ({
345
+ pos,
346
+ diffId,
347
+ anchorType,
348
+ side
349
+ }) => Decoration.widget(pos, () => createAnchorNameSpan(buildAnchorDecorationKey({
350
+ diffId,
351
+ anchorType
352
+ })), buildAnchorDecorationSpec({
353
+ diffId,
354
+ anchorType,
355
+ side
356
+ }));
357
+
332
358
  /**
333
359
  * Invisible `from`/`to` (and optional `left`) anchor widgets for one inline diff
334
360
  * range, so the `IndicatorBar` can align itself via CSS anchor positioning.
@@ -355,6 +381,22 @@ export const createInlineIndicatorAnchorWidgets = ({
355
381
  * IndicatorBar can determine top/bottom even if
356
382
  * the inline decoration is broken up by marks / between blocks.
357
383
  */
384
+ // Gated purely so the `createAnchorSpanWidget` rewrite can be rolled back independently of the
385
+ // contributor-tag anchors it was extracted for: both branches build the same two widgets at the
386
+ // same positions. Drop the fallback with the gate.
387
+ if (fg('confluence_ncs_step_diffing_version_history')) {
388
+ return [createAnchorSpanWidget({
389
+ pos: fromPos,
390
+ diffId,
391
+ anchorType: AnchorTypeKey.from,
392
+ side: 1
393
+ }), createAnchorSpanWidget({
394
+ pos: toPos,
395
+ diffId,
396
+ anchorType: AnchorTypeKey.to,
397
+ side: -1
398
+ }), ...maybeLeftAnchor];
399
+ }
358
400
  const fromAnchorKey = buildAnchorDecorationKey({
359
401
  diffId,
360
402
  anchorType: AnchorTypeKey.from
@@ -0,0 +1,163 @@
1
+ import { createElement } from 'react';
2
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
4
+ import { ContributorTagWidget } from '../../ui/ContributorTag/ContributorTagWidget';
5
+ import { clampAnchorPosIntoCell } from './createAnchorDecorationWidgets';
6
+ import { buildContributorTagDecorationSpec, DecorationFamily } from './decorationKeys';
7
+
8
+ /** Marks the host element of one tag, so a tag can be found in the document by its own diff. */
9
+ export const CONTRIBUTOR_TAG_HOST_ATTRIBUTE = 'data-contributor-tag-host';
10
+
11
+ // Contributor tags only render behind this gate — see `areAttributionColorGatesEnabled`.
12
+ const isContributorTagWidgetEnabled = () => fg('confluence_ncs_step_diffing_version_history');
13
+
14
+ /**
15
+ * Everything a tag needs to mount itself into the document, plumbed down from the plugin. Absent in
16
+ * unit tests that build decorations directly: the hosts are still emitted, they just stay empty.
17
+ */
18
+
19
+ /** A mounted tag, so whoever emitted the host can take it down again. */
20
+
21
+ /**
22
+ * Unique per mount rather than per diff, because ProseMirror draws a replacement widget before it
23
+ * destroys the widget being replaced: on a shared key the outgoing widget's `destroy` would
24
+ * unregister the incoming widget's portal.
25
+ */
26
+ let nextContributorTagPortalId = 0;
27
+
28
+ /**
29
+ * Renders the tag into `host` through the editor's portal provider, the way the rest of the editor
30
+ * mounts React into decorations. The tag resolves its own model from plugin state, so a redraw of
31
+ * the host re-parents the tag instead of stranding it, and `isActive` changes arrive as a re-render.
32
+ */
33
+ export const mountContributorTag = ({
34
+ diffId,
35
+ host,
36
+ mountContext
37
+ }) => {
38
+ if (!mountContext) {
39
+ return undefined;
40
+ }
41
+ const {
42
+ api,
43
+ getEditorRoot,
44
+ portalProviderAPI
45
+ } = mountContext;
46
+ const key = `${DecorationFamily.contributorTag}-${diffId}-${nextContributorTagPortalId++}`;
47
+ portalProviderAPI.render(() => /*#__PURE__*/createElement(ContributorTagWidget, {
48
+ api,
49
+ diffId,
50
+ getEditorRoot
51
+ }), host, key);
52
+ return {
53
+ key,
54
+ portalProviderAPI
55
+ };
56
+ };
57
+ export const unmountContributorTag = mount => {
58
+ mount === null || mount === void 0 ? void 0 : mount.portalProviderAPI.remove(mount.key);
59
+ };
60
+
61
+ /**
62
+ * A zero-size inline host for one contributor tag. `position: relative` makes it the containing
63
+ * block of the absolutely positioned tag, so the tag sits on the change with no measurement.
64
+ */
65
+ const buildContributorTagHost = diffId => {
66
+ const host = document.createElement('span');
67
+ host.setAttribute(CONTRIBUTOR_TAG_HOST_ATTRIBUTE, diffId);
68
+ host.style.setProperty('position', 'relative');
69
+ return host;
70
+ };
71
+
72
+ /**
73
+ * The host for a tag on deleted content, which renders inside its own widget decoration rather than
74
+ * needing one of its own. Unmount with `unmountContributorTag` from that widget's `destroy`.
75
+ */
76
+ export const createContributorTagHost = (diffId, mountContext) => {
77
+ if (!isContributorTagWidgetEnabled()) {
78
+ return undefined;
79
+ }
80
+ const host = buildContributorTagHost(diffId);
81
+ return {
82
+ host,
83
+ mount: mountContributorTag({
84
+ diffId,
85
+ host,
86
+ mountContext
87
+ })
88
+ };
89
+ };
90
+
91
+ /**
92
+ * The start of the inline content that `[from, to)` actually highlights.
93
+ *
94
+ * `from` is not sufficient: a range can begin at the tail of one paragraph and only cover text in a
95
+ * later block, and ProseMirror clips `Decoration.inline` to the content it can paint — so a host
96
+ * left at `from` would sit lines away from the highlight.
97
+ */
98
+ const resolveInlineContentStart = (doc, from, to) => {
99
+ var _edge;
100
+ let edge;
101
+ doc.nodesBetween(from, to, (node, nodePos) => {
102
+ if (!node.isTextblock) {
103
+ return edge === undefined;
104
+ }
105
+
106
+ // The slice of this block the range actually covers; empty means nothing paintable.
107
+ const contentFrom = Math.max(nodePos + 1, from);
108
+ const contentTo = Math.min(nodePos + 1 + node.content.size, to);
109
+ if (contentTo > contentFrom && edge === undefined) {
110
+ edge = contentFrom;
111
+ }
112
+
113
+ // Textblocks have no block children to descend into.
114
+ return false;
115
+ });
116
+ return (_edge = edge) !== null && _edge !== void 0 ? _edge : from;
117
+ };
118
+
119
+ /**
120
+ * A widget hosting the contributor tag of one inline diff range, placed on the first character the
121
+ * range highlights.
122
+ *
123
+ * The tag is mounted in `toDOM` and taken down in `destroy`, so it lives exactly as long as the host
124
+ * element ProseMirror drew it into — see `mountContributorTag`.
125
+ */
126
+ export const createContributorTagWidget = ({
127
+ doc,
128
+ from,
129
+ to,
130
+ diffId,
131
+ mountContext
132
+ }) => {
133
+ if (!isContributorTagWidgetEnabled()) {
134
+ return undefined;
135
+ }
136
+
137
+ // Reassigned when ProseMirror redraws this decoration, which it may do more than once for the
138
+ // same `Decoration` instance.
139
+ let mount;
140
+ return Decoration.widget(
141
+ // Keep the host out of the table row's grid (EDITOR-8442).
142
+ clampAnchorPosIntoCell(doc, resolveInlineContentStart(doc, from, to), 1), () => {
143
+ const host = buildContributorTagHost(diffId);
144
+ mount = mountContributorTag({
145
+ diffId,
146
+ host,
147
+ mountContext
148
+ });
149
+ return host;
150
+ }, {
151
+ ...buildContributorTagDecorationSpec(diffId),
152
+ side: 1,
153
+ // The tag is chrome: it inherits no surrounding mark, and its own events are not the
154
+ // document's to handle.
155
+ marks: [],
156
+ ignoreSelection: true,
157
+ stopEvent: () => true,
158
+ destroy: () => {
159
+ unmountContributorTag(mount);
160
+ mount = undefined;
161
+ }
162
+ });
163
+ };