@atlaskit/editor-plugin-show-diff 15.0.0 → 15.1.1

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 (63) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/afm-cc/tsconfig.json +0 -12
  3. package/afm-products/tsconfig.json +0 -12
  4. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +131 -79
  5. package/dist/cjs/pm-plugins/decorations/colorSchemes/factory.js +32 -0
  6. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +87 -25
  7. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +27 -4
  8. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +17 -3
  9. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  10. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +43 -14
  11. package/dist/cjs/pm-plugins/decorations/revealStyles.js +156 -0
  12. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -5
  13. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -2
  14. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  15. package/dist/cjs/pm-plugins/main.js +28 -10
  16. package/dist/cjs/pm-plugins/resolveDiffContributors.js +4 -0
  17. package/dist/cjs/pm-plugins/revealAnimation.js +524 -0
  18. package/dist/cjs/showDiffPlugin.js +42 -7
  19. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +59 -9
  20. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +30 -0
  21. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +62 -4
  22. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +25 -3
  23. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +18 -1
  24. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  25. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +36 -9
  26. package/dist/es2019/pm-plugins/decorations/revealStyles.js +139 -0
  27. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -6
  28. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +6 -3
  29. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +5 -2
  30. package/dist/es2019/pm-plugins/main.js +28 -9
  31. package/dist/es2019/pm-plugins/resolveDiffContributors.js +4 -0
  32. package/dist/es2019/pm-plugins/revealAnimation.js +495 -0
  33. package/dist/es2019/showDiffPlugin.js +37 -7
  34. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +131 -79
  35. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +30 -0
  36. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +87 -25
  37. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +27 -4
  38. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +17 -3
  39. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  40. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +43 -14
  41. package/dist/esm/pm-plugins/decorations/revealStyles.js +149 -0
  42. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -5
  43. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -2
  44. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  45. package/dist/esm/pm-plugins/main.js +28 -10
  46. package/dist/esm/pm-plugins/resolveDiffContributors.js +4 -0
  47. package/dist/esm/pm-plugins/revealAnimation.js +518 -0
  48. package/dist/esm/showDiffPlugin.js +42 -7
  49. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  50. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +2 -1
  51. package/dist/types/pm-plugins/decorations/colorSchemes/factory.d.ts +24 -0
  52. package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +5 -1
  53. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +9 -3
  54. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +3 -2
  55. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +3 -2
  56. package/dist/types/pm-plugins/decorations/revealStyles.d.ts +49 -0
  57. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +4 -3
  58. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.d.ts +1 -1
  59. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.d.ts +1 -1
  60. package/dist/types/pm-plugins/main.d.ts +6 -1
  61. package/dist/types/pm-plugins/revealAnimation.d.ts +33 -0
  62. package/dist/types/showDiffPluginType.d.ts +29 -1
  63. package/package.json +10 -4
@@ -157,6 +157,7 @@ const tableRanges = (doc, from, to) => {
157
157
  };
158
158
  const isInsideTable = (tables, pos) => tables.some(([tFrom, tTo]) => pos >= tFrom && pos < tTo);
159
159
  const calculateNodesForBlockDecoration = ({
160
+ attributionKey,
160
161
  doc,
161
162
  from,
162
163
  to,
@@ -164,9 +165,11 @@ const calculateNodesForBlockDecoration = ({
164
165
  isInserted = true,
165
166
  activeIndexPos,
166
167
  shouldHideDeleted = false,
168
+ showContributorTags = false,
167
169
  showIndicators = false,
168
170
  diffType,
169
- coarseTableCellsOnly = false
171
+ coarseTableCellsOnly = false,
172
+ tagMountContext
170
173
  }) => {
171
174
  const decorations = [];
172
175
  // Coarse path: inside the large table, only cell/header overlays are kept — the
@@ -185,6 +188,7 @@ const calculateNodesForBlockDecoration = ({
185
188
  const nodeEnd = pos + node.nodeSize;
186
189
  const isActive = isRangeActive(activeIndexPos, pos, nodeEnd);
187
190
  decorations.push(...createBlockChangedDecoration({
191
+ attributionKey,
188
192
  change: {
189
193
  from: pos,
190
194
  to: nodeEnd,
@@ -194,9 +198,11 @@ const calculateNodesForBlockDecoration = ({
194
198
  isInserted,
195
199
  isActive,
196
200
  shouldHideDeleted,
201
+ showContributorTags,
197
202
  showIndicators,
198
203
  doc,
199
- diffType
204
+ diffType,
205
+ tagMountContext
200
206
  }));
201
207
  }
202
208
  });
@@ -226,6 +232,25 @@ export const isDeletedContentPlacedBelow = ({
226
232
  // Inline-level (undefined) and sentence-level changes.
227
233
  return inlineDeletedDiffPlacement === 'after';
228
234
  };
235
+
236
+ /**
237
+ * Single place `platform_editor_diff_hide_pure_deletions` is read, shared by the widget-anchor
238
+ * calculation and the decoration push below so the two cannot drift.
239
+ *
240
+ * The clean view ("new state") historically only suppressed a deleted side when the same change
241
+ * also carried an insertion. A PURE deletion — text removed with nothing put in its place — fell
242
+ * through and leaked struck-through text into a view that is supposed to show none. `smart` never
243
+ * showed this because it pairs a rewrite into a single change that always carries an insertion;
244
+ * `inline` emits delete-only changes, so it did.
245
+ *
246
+ * With the gate off this returns exactly what both callers computed before it existed.
247
+ */
248
+ const shouldHideDeletedSide = ({
249
+ change,
250
+ diffType,
251
+ hideDeletedDiffs,
252
+ isInverted
253
+ }) => isExtendedEnabled(diffType) && !isInverted && !!hideDeletedDiffs && (change.inserted.length > 0 || fg('platform_editor_diff_hide_pure_deletions'));
229
254
  const calculateDiffDecorationsInner = ({
230
255
  state,
231
256
  pluginState,
@@ -242,6 +267,7 @@ const calculateDiffDecorationsInner = ({
242
267
  smartThresholds,
243
268
  deletedDiffPlacement = 'top',
244
269
  inlineDeletedDiffPlacement = 'before',
270
+ reveal,
245
271
  tagMountContext
246
272
  }) => {
247
273
  const {
@@ -457,7 +483,12 @@ const calculateDiffDecorationsInner = ({
457
483
  // change. Used to decide if indicator anchor positions should be adjusted
458
484
  // inward — when the widget is present the anchor must stay at the block
459
485
  // boundary to keep the indicator bar continuous with the deleted content.
460
- const willRenderDeletedWidget = change.deleted.length > 0 && !isMarkOnly && !(isExtendedEnabled(diffType) && !isInverted && hideDeletedDiffs && change.inserted.length > 0);
486
+ const willRenderDeletedWidget = change.deleted.length > 0 && !isMarkOnly && !shouldHideDeletedSide({
487
+ change,
488
+ diffType,
489
+ hideDeletedDiffs,
490
+ isInverted
491
+ });
461
492
  if (isSmartNodeLevel) {
462
493
  // For a large inserted table, skip the O(cells) inline decorations inside the
463
494
  // table (the cell overlays added below provide the highlight without the
@@ -485,6 +516,7 @@ const calculateDiffDecorationsInner = ({
485
516
  hideAddedDiffsUnderline,
486
517
  hasDeletedWidget: willRenderDeletedWidget,
487
518
  isDeletedWidgetBelow,
519
+ reveal,
488
520
  tagMountContext
489
521
  })
490
522
  }));
@@ -505,11 +537,13 @@ const calculateDiffDecorationsInner = ({
505
537
  hideAddedDiffsUnderline,
506
538
  hasDeletedWidget: willRenderDeletedWidget,
507
539
  isDeletedWidgetBelow,
540
+ reveal,
508
541
  tagMountContext
509
542
  })
510
543
  }));
511
544
  }
512
545
  decorations.push(...calculateNodesForBlockDecoration({
546
+ attributionKey,
513
547
  doc: tr.doc,
514
548
  from: change.fromB,
515
549
  to: change.toB,
@@ -517,7 +551,9 @@ const calculateDiffDecorationsInner = ({
517
551
  ...(isExtendedEnabled(diffType) && {
518
552
  isInserted,
519
553
  shouldHideDeleted,
520
- showIndicators
554
+ showContributorTags,
555
+ showIndicators,
556
+ tagMountContext
521
557
  }),
522
558
  activeIndexPos,
523
559
  intl,
@@ -526,11 +562,19 @@ const calculateDiffDecorationsInner = ({
526
562
  }));
527
563
  }
528
564
  if (change.deleted.length > 0 && !isMarkOnly) {
529
- const shouldHideDeleted = isExtendedEnabled(diffType) ? !isInverted && hideDeletedDiffs && change.inserted.length > 0 : false;
565
+ const shouldHideDeleted = shouldHideDeletedSide({
566
+ change,
567
+ diffType,
568
+ hideDeletedDiffs,
569
+ isInverted
570
+ });
530
571
  if (!shouldHideDeleted) {
531
572
  decorations.push(...createNodeChangedDecorationWidget({
532
573
  // The deleted side has no inline decoration, so the widget carries the actor.
533
574
  attributionKey,
575
+ // Extended pipeline only, matching the inline path: the non-extended styles have no
576
+ // background for the wipe to act on.
577
+ reveal: isExtendedEnabled(diffType) ? reveal : undefined,
534
578
  change,
535
579
  doc: originalDoc,
536
580
  nodeViewSerializer,
@@ -568,7 +612,8 @@ const calculateDiffDecorationsInner = ({
568
612
  colorScheme,
569
613
  diffType,
570
614
  isActive,
571
- isInserted: true
615
+ isInserted: true,
616
+ reveal
572
617
  }));
573
618
  });
574
619
  getAttrChangeRanges(tr.doc, attrStepContexts, originalDoc).forEach(change => {
@@ -671,7 +716,8 @@ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner
671
716
  showIndicators,
672
717
  smartThresholds,
673
718
  deletedDiffPlacement,
674
- inlineDeletedDiffPlacement
719
+ inlineDeletedDiffPlacement,
720
+ reveal
675
721
  }], [{
676
722
  pluginState: lastPluginState,
677
723
  state: lastState,
@@ -685,7 +731,8 @@ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner
685
731
  showIndicators: lastShowIndicators,
686
732
  smartThresholds: lastSmartThresholds,
687
733
  deletedDiffPlacement: lastDeletedDiffPlacement,
688
- inlineDeletedDiffPlacement: lastInlineDeletedDiffPlacement
734
+ inlineDeletedDiffPlacement: lastInlineDeletedDiffPlacement,
735
+ reveal: lastReveal
689
736
  }]) => {
690
737
  var _ref2;
691
738
  const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
@@ -693,7 +740,10 @@ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner
693
740
  var _ref;
694
741
  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) &&
695
742
  // Contributors can arrive after the first calculation; without this, tags never appear.
696
- 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;
743
+ isEqual(pluginState.contributors, lastPluginState.contributors) && state.doc.eq(lastState.doc) && hideDeletedDiffs === lastHideDeletedDiffs && hideAddedDiffsUnderline === lastHideAddedDiffsUnderline && showIndicators === lastShowIndicators && isEqual(smartThresholds, lastSmartThresholds) && deletedDiffPlacement === lastDeletedDiffPlacement && inlineDeletedDiffPlacement === lastInlineDeletedDiffPlacement &&
744
+ // Turning the reveal on or off changes the emitted styles, so a cached result from the
745
+ // other setting would leave the diff painted in the wrong resting state.
746
+ isEqual(reveal, lastReveal)) !== null && _ref !== void 0 ? _ref : false;
697
747
  }
698
748
  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;
699
749
  });
@@ -683,6 +683,36 @@ export function buildInsertedInlineStyle(colors, isActive, hideAddedDiffsUnderli
683
683
  return hideAddedDiffsUnderline ? buildInsertStyleExtendedNoUnderline(colors) : buildInsertStyleExtended(colors);
684
684
  }
685
685
 
686
+ /**
687
+ * The colours `buildInsertedInlineStyle()` above would paint, for the reveal to animate towards.
688
+ *
689
+ * MUST mirror that function: the reveal replaces the flat highlight with a sized gradient, so a
690
+ * divergence here means the animation settles on a different colour than a static render produces.
691
+ * `revealStyles.ts` in the test package asserts the two agree across the isActive /
692
+ * hideAddedDiffsUnderline matrix.
693
+ *
694
+ * A transparent border is how "no underline" is expressed: the box still reserves the 2px so the
695
+ * text does not shift when the underline arrives.
696
+ */
697
+ export function getInsertedInlineRevealColors(colors, isActive, hideAddedDiffsUnderline) {
698
+ return {
699
+ background: isActive ? bgSubtlerPressed(colors.insertActiveColor) : bgSubtlest(colors.insertColor),
700
+ border: hideAddedDiffsUnderline ? 'transparent' : borderAccent(colors.insertColor)
701
+ };
702
+ }
703
+
704
+ /**
705
+ * The colours `buildDeletedInlineContentStyleExtended()` paints, for the reveal to animate towards.
706
+ * Deleted content's highlight does not vary with the active state — only its text does, which the
707
+ * reveal keeps as-is.
708
+ */
709
+ export function getDeletedInlineRevealColors(colors) {
710
+ return {
711
+ background: bgSubtlest(colors.deleteColor),
712
+ border: borderAccent(colors.deleteColor)
713
+ };
714
+ }
715
+
686
716
  /** The three visual states deleted inline content can be in. */
687
717
 
688
718
  /**
@@ -1,10 +1,12 @@
1
1
  import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
2
+ import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
2
3
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
3
4
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
5
  import { isExtendedEnabled } from '../isExtendedEnabled';
5
6
  import { buildAddedCellOverlayRoundedStyle, buildAddedCellOverlayStyle, buildDeletedBlockNodeStyle, buildDeletedCellOverlayRoundedStyle, buildDeletedCellOverlayStyle, buildInsertedBlockNodeStyle } from './colorSchemes/factory';
6
7
  import { colorSchemeRegistry, getLegacyColorScheme } from './colorSchemes/schemes';
7
8
  import { createBlockIndicatorAnchorWidgets } from './createAnchorDecorationWidgets';
9
+ import { createContributorTagWidget } from './createContributorTagWidget';
8
10
  import { buildDiffDecorationSpec } from './decorationKeys';
9
11
  import { getBlockNodeStyleLegacy, resolveCellOverlayStyleLegacy } from './createBlockChangedDecoration.styles.legacy';
10
12
  const displayNoneStyle = convertToInlineCss({
@@ -20,6 +22,24 @@ const UNSTYLED_NODES = ['mediaSingle', 'mediaGroup', 'table',
20
22
  'heading', 'hardBreak', 'decisionList', 'taskList', 'bulletList', 'orderedList', 'layoutSection'];
21
23
  const CELL_NODES = ['tableCell', 'tableHeader'];
22
24
 
25
+ /**
26
+ * Block nodes that host a contributor tag of their own — those whose whole box reads as one change.
27
+ * Lists and tables are excluded: they decorate per item and per cell, with no one-tag-per-block
28
+ * design yet. `blockCard` covers the datasource variant, which is the same node type.
29
+ */
30
+ const TAGGABLE_BLOCK_NODES = new Set(['blockCard', 'blockquote', 'bodiedExtension', 'codeBlock', 'embedCard', 'expand', 'extension', 'media', 'multiBodiedExtension', 'panel', 'rule']);
31
+
32
+ /** Panels, rules and media can also sit in a table cell, which is out of scope for block tags. */
33
+ const isInsideTable = (doc, pos) => findParentNodeClosestToPos(doc.resolve(pos), node => node.type.name === 'table') !== undefined;
34
+
35
+ /** Whether this block can host a contributor tag of its own. */
36
+ const canTagBlock = ({
37
+ diffType,
38
+ doc,
39
+ from,
40
+ name
41
+ }) => doc !== undefined && isExtendedEnabled(diffType) && TAGGABLE_BLOCK_NODES.has(name) && !isInsideTable(doc, from);
42
+
23
43
  /** Positioning context for the cell overlay widget decorations. */
24
44
  const cellPositionStyle = convertToInlineCss({
25
45
  position: 'relative'
@@ -134,17 +154,28 @@ const isCellEmpty = cellNode => {
134
154
  * @returns Prosemirror node decoration or undefined
135
155
  */
136
156
  export const createBlockChangedDecoration = ({
157
+ attributionKey,
137
158
  change,
138
159
  colorScheme,
139
160
  isInserted = true,
140
161
  isActive = false,
141
162
  shouldHideDeleted = false,
163
+ showContributorTags = false,
142
164
  showIndicators = false,
143
165
  doc,
144
- diffType
166
+ diffType,
167
+ tagMountContext
145
168
  }) => {
146
169
  const decorations = [];
147
- const diffId = crypto.randomUUID();
170
+ // Derived from the node range so it survives a recalculation, as in
171
+ // `createInlineChangedDecoration`. Changes are disjoint, so no two of them decorate one node.
172
+ const diffId = showContributorTags ? `block-${change.from}-${change.to}` : crypto.randomUUID();
173
+ const shouldTagBlock = showContributorTags && canTagBlock({
174
+ diffType,
175
+ doc,
176
+ from: change.from,
177
+ name: change.name
178
+ });
148
179
  if (shouldHideDeleted) {
149
180
  return [Decoration.node(change.from, change.to, {
150
181
  style: displayNoneStyle
@@ -193,16 +224,26 @@ export const createBlockChangedDecoration = ({
193
224
  decorations.push(Decoration.node(change.from, change.to, {
194
225
  style: style,
195
226
  'data-testid': 'show-diff-changed-decoration-node',
196
- class: className
227
+ class: className,
228
+ // Lets the contributor tag find the block it captions on hover.
229
+ ...(shouldTagBlock && {
230
+ 'data-diff-id': diffId
231
+ })
197
232
  }, buildDiffDecorationSpec({
233
+ attributionKey: shouldTagBlock ? attributionKey : undefined,
234
+ colorScheme,
198
235
  decorationType: 'block',
199
236
  diffId,
200
237
  isActive,
238
+ isInserted,
201
239
  nodeName: change.name,
202
240
  diffType
203
241
  })));
204
242
  }
205
- if (decorations.length > 0 && showIndicators && doc && isExtendedEnabled(diffType)) {
243
+ if (decorations.length === 0) {
244
+ return decorations;
245
+ }
246
+ if (showIndicators && doc && isExtendedEnabled(diffType)) {
206
247
  decorations.push(...createBlockIndicatorAnchorWidgets({
207
248
  doc,
208
249
  from: change.from,
@@ -210,5 +251,22 @@ export const createBlockChangedDecoration = ({
210
251
  diffId
211
252
  }));
212
253
  }
254
+
255
+ // `change.from` is just before the node, so the host sits outside it and the tag renders on the
256
+ // block's own top-left corner. Resolving inline content instead would put the host inside a
257
+ // container block's content DOM (panel, expand, blockquote).
258
+ if (shouldTagBlock && doc) {
259
+ const tagWidget = createContributorTagWidget({
260
+ anchorAtRangeStart: true,
261
+ doc,
262
+ from: change.from,
263
+ to: change.to,
264
+ diffId,
265
+ mountContext: tagMountContext
266
+ });
267
+ if (tagWidget) {
268
+ decorations.push(tagWidget);
269
+ }
270
+ }
213
271
  return decorations;
214
272
  };
@@ -103,13 +103,32 @@ const resolveInlineContentStart = (doc, from, to) => {
103
103
  };
104
104
 
105
105
  /**
106
- * A widget hosting the contributor tag of one inline diff range, placed on the first character the
107
- * range highlights.
106
+ * The position just before the code block enclosing `pos`, or `undefined` when nothing does.
107
+ *
108
+ * `.code-block-content-wrapper` hides its block-axis overflow, so a host left inside the code has
109
+ * its tag clipped away. Hosting it before the block puts the tag on the block's top-left corner,
110
+ * where a whole-code-block change is tagged too (EDITOR-8766).
111
+ */
112
+ const resolveCodeBlockStart = (doc, pos) => {
113
+ const $pos = doc.resolve(pos);
114
+ for (let depth = $pos.depth; depth > 0; depth--) {
115
+ if ($pos.node(depth).type.name === 'codeBlock') {
116
+ return $pos.before(depth);
117
+ }
118
+ }
119
+ return undefined;
120
+ };
121
+
122
+ /**
123
+ * A widget hosting the contributor tag of one diff range, placed on the first character the range
124
+ * highlights — or, with `anchorAtRangeStart`, at `from` itself. Either way a host that would land
125
+ * inside a code block is hoisted out in front of it; see `resolveCodeBlockStart`.
108
126
  *
109
127
  * The tag is mounted in `toDOM` and taken down in `destroy`, so it lives exactly as long as the host
110
128
  * element ProseMirror drew it into — see `mountContributorTag`.
111
129
  */
112
130
  export const createContributorTagWidget = ({
131
+ anchorAtRangeStart = false,
113
132
  doc,
114
133
  from,
115
134
  to,
@@ -123,9 +142,12 @@ export const createContributorTagWidget = ({
123
142
  // Reassigned when ProseMirror redraws this decoration, which it may do more than once for the
124
143
  // same `Decoration` instance.
125
144
  let mount;
145
+ const anchorPos = anchorAtRangeStart ? from : resolveInlineContentStart(doc, from, to);
146
+ // A change inside a code block is hoisted out of it; anything else keeps its own anchor.
147
+ const codeBlockPos = resolveCodeBlockStart(doc, anchorPos);
126
148
  return Decoration.widget(
127
149
  // Keep the host out of the table row's grid (EDITOR-8442).
128
- clampAnchorPosIntoCell(doc, resolveInlineContentStart(doc, from, to), 1), () => {
150
+ clampAnchorPosIntoCell(doc, codeBlockPos !== null && codeBlockPos !== void 0 ? codeBlockPos : anchorPos, 1), () => {
129
151
  const host = buildContributorTagHost(diffId);
130
152
  mount = mountContributorTag({
131
153
  diffId,
@@ -8,6 +8,7 @@ import { createInlineIndicatorAnchorWidgets } from './createAnchorDecorationWidg
8
8
  import { createContributorTagWidget } from './createContributorTagWidget';
9
9
  import { getAtomicInlineNodeClassNameLegacy, resolveInlineChangedStyleLegacy } from './createInlineChangedDecoration.styles.legacy';
10
10
  import { buildDiffDecorationSpec } from './decorationKeys';
11
+ import { REVEAL_ATTR, resolveRevealStyle } from './revealStyles';
11
12
  const displayNoneStyle = convertToInlineCss({
12
13
  display: 'none'
13
14
  });
@@ -107,8 +108,10 @@ export const createInlineChangedDecoration = ({
107
108
  inlineNodeName,
108
109
  hasDeletedWidget = false,
109
110
  isDeletedWidgetBelow = false,
111
+ reveal,
110
112
  tagMountContext
111
113
  }) => {
114
+ var _revealed$style;
112
115
  // Derived from the range so it survives a recalculation: stepping to the next/previous change
113
116
  // rebuilds decorations without touching the doc, and a fresh id would change the React key and the
114
117
  // tag host's key, remounting every tag mid-step. Random ids are kept when tags are off.
@@ -127,7 +130,18 @@ export const createInlineChangedDecoration = ({
127
130
  }))];
128
131
  }
129
132
  const colors = getColorScheme(colorScheme);
130
- const style = resolveInlineChangedStyle({
133
+
134
+ // The reveal withholds the static highlight so the wipe has something to reveal, so it supplies
135
+ // the whole style rather than adding to the usual one. Extended pipeline only — the AI review
136
+ // surface is the only consumer, and the non-extended styles have no background to wipe.
137
+ const revealed = isExtendedEnabled(diffType) ? resolveRevealStyle({
138
+ colorScheme,
139
+ hideAddedDiffsUnderline,
140
+ isActive,
141
+ isInserted,
142
+ reveal
143
+ }) : undefined;
144
+ const style = (_revealed$style = revealed === null || revealed === void 0 ? void 0 : revealed.style) !== null && _revealed$style !== void 0 ? _revealed$style : resolveInlineChangedStyle({
131
145
  colors,
132
146
  colorScheme,
133
147
  diffType,
@@ -142,6 +156,9 @@ export const createInlineChangedDecoration = ({
142
156
  ...(atomicInlineAttrs && {
143
157
  class: atomicInlineAttrs.className
144
158
  }),
159
+ ...(revealed && {
160
+ [REVEAL_ATTR]: revealed.role
161
+ }),
145
162
  'data-testid': 'show-diff-changed-decoration',
146
163
  // Lets the contributor tag find its highlight on hover.
147
164
  ...(showContributorTags && {
@@ -108,6 +108,7 @@ export const createNodeChangedDecorationWidget = ({
108
108
  placeBelow = false,
109
109
  diffType,
110
110
  hideAddedDiffsUnderline = false,
111
+ reveal,
111
112
  tagMountContext
112
113
  }) => {
113
114
  var _slice$content, _slice$content2, _slice$content2$first, _slice$content3, _slice$content3$first, _$safeInsertPos$nodeB, _slice$content$firstC, _newDoc$firstChild, _slice$content$firstC2;
@@ -277,7 +278,7 @@ export const createNodeChangedDecorationWidget = ({
277
278
  const nodeView = serializer.tryCreateNodeView(node);
278
279
  if (nodeView) {
279
280
  if (node.isInline) {
280
- const wrapper = createContentWrapper(colorScheme, isActive, isInserted, diffType);
281
+ const wrapper = createContentWrapper(colorScheme, isActive, isInserted, diffType, reveal);
281
282
  wrapper.append(nodeView);
282
283
  dom.append(wrapper);
283
284
  } else {
@@ -307,11 +308,12 @@ export const createNodeChangedDecorationWidget = ({
307
308
  colorScheme,
308
309
  isActive,
309
310
  isInserted,
310
- diffType
311
+ diffType,
312
+ reveal
311
313
  });
312
314
  dom.append(injectedNode);
313
315
  } else {
314
- const wrapper = createContentWrapper(colorScheme, isActive, isInserted, diffType);
316
+ const wrapper = createContentWrapper(colorScheme, isActive, isInserted, diffType, reveal);
315
317
  wrapper.append(fallbackNode);
316
318
  dom.append(wrapper);
317
319
  }
@@ -2,9 +2,9 @@ import { isDiffDecoration, isDiffDecorationSpec } from './decorationKeys';
2
2
 
3
3
  /**
4
4
  * Diff kinds that expose an anchor a tag can be pinned to: `inline` is added/changed content,
5
- * `widget` is deleted content. Block diffs carry no attribution.
5
+ * `widget` is deleted content, `block` a whole changed block node.
6
6
  */
7
- const TAGGABLE_DECORATION_TYPES = new Set(['inline', 'widget']);
7
+ const TAGGABLE_DECORATION_TYPES = new Set(['block', 'inline', 'widget']);
8
8
 
9
9
  /** The connection is an internal key, so it is dropped on the way to the tag. */
10
10
  const toTagContributor = ({
@@ -18,6 +18,13 @@ const toTagContributor = ({
18
18
  */
19
19
  const isSameChange = (widget, inline) => widget.spec.attributionKey === inline.spec.attributionKey && widget.from >= inline.from && widget.from <= inline.to;
20
20
 
21
+ /**
22
+ * Whether `inner` captions the same change as the block decoration `block`, in which case only the
23
+ * block keeps the tag — one tag per block. Containment counts in either direction: a leaf block's
24
+ * highlight spans the wrapper the block sits in. Merely adjacent ranges keep a tag each.
25
+ */
26
+ const isSameBlockChange = (inner, block) => inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
27
+
21
28
  /**
22
29
  * One model per diff decoration whose attribution resolves to a supplied contributor; anything
23
30
  * unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
@@ -52,22 +59,42 @@ export const extractContributorTags = (decorations, contributors) => {
52
59
  const inlineTargets = targets.filter(({
53
60
  decoration
54
61
  }) => decoration.spec.decorationType === 'inline');
62
+ const blockTargets = targets.filter(({
63
+ decoration
64
+ }) => decoration.spec.decorationType === 'block');
55
65
  const linkedDiffIds = new Map();
56
66
  const folded = new Set();
57
- for (const target of targets) {
67
+ const fold = (host, target) => {
58
68
  var _linkedDiffIds$get;
59
- if (target.decoration.spec.decorationType !== 'widget') {
69
+ folded.add(target);
70
+ const hostDiffId = host.decoration.spec.diffId;
71
+ linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), target.decoration.spec.diffId]);
72
+ };
73
+ for (const target of targets) {
74
+ const {
75
+ decorationType
76
+ } = target.decoration.spec;
77
+ if (decorationType === 'block') {
78
+ continue;
79
+ }
80
+
81
+ // A block outranks both other kinds, so it is tried first.
82
+ const block = blockTargets.find(({
83
+ decoration
84
+ }) => isSameBlockChange(target.decoration, decoration));
85
+ if (block) {
86
+ fold(block, target);
87
+ continue;
88
+ }
89
+ if (decorationType !== 'widget') {
60
90
  continue;
61
91
  }
62
92
  const host = inlineTargets.find(({
63
93
  decoration
64
94
  }) => isSameChange(target.decoration, decoration));
65
- if (!host) {
66
- continue;
95
+ if (host) {
96
+ fold(host, target);
67
97
  }
68
- folded.add(target);
69
- const hostDiffId = host.decoration.spec.diffId;
70
- linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : []), target.decoration.spec.diffId]);
71
98
  }
72
99
  return targets.filter(target => !folded.has(target)).map(({
73
100
  contributor,