Преглед на файлове

Merge pull request #20411 from overleaf/dk-highlight-focus-ranges

Highlight and focus ranges in editor for new review panel

GitOrigin-RevId: 4fef31a8c9e6dc974519d925c7478665e0c8cc29
David преди 1 година
родител
ревизия
22e4c2de2c

+ 3 - 0
services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx

@@ -8,6 +8,7 @@ import { isSelectionWithinOp } from '../utils/is-selection-within-op'
 import { EditorSelection } from '@codemirror/state'
 import { EditorSelection } from '@codemirror/state'
 import { EditorView } from '@codemirror/view'
 import { EditorView } from '@codemirror/view'
 import classNames from 'classnames'
 import classNames from 'classnames'
+import { highlightRanges } from '@/features/source-editor/extensions/ranges'
 
 
 export const ReviewPanelEntry: FC<{
 export const ReviewPanelEntry: FC<{
   position: number
   position: number
@@ -40,6 +41,8 @@ export const ReviewPanelEntry: FC<{
     <div
     <div
       onFocus={focusHandler}
       onFocus={focusHandler}
       onBlur={() => setFocused(false)}
       onBlur={() => setFocused(false)}
+      onMouseEnter={() => view.dispatch(highlightRanges(op))}
+      onMouseLeave={() => view.dispatch(highlightRanges())}
       role="button"
       role="button"
       tabIndex={position + 1}
       tabIndex={position + 1}
       className={classNames(
       className={classNames(

+ 127 - 2
services/web/frontend/js/features/source-editor/extensions/ranges.ts

@@ -7,12 +7,21 @@ import {
   ViewPlugin,
   ViewPlugin,
   WidgetType,
   WidgetType,
 } from '@codemirror/view'
 } from '@codemirror/view'
-import { Change, DeleteOperation } from '../../../../../types/change'
+import {
+  AnyOperation,
+  Change,
+  DeleteOperation,
+} from '../../../../../types/change'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
-import { isCommentOperation, isDeleteOperation } from '@/utils/operations'
+import {
+  isCommentOperation,
+  isDeleteOperation,
+  isInsertOperation,
+} from '@/utils/operations'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
 import { Ranges } from '@/features/review-panel-new/context/ranges-context'
 import { Ranges } from '@/features/review-panel-new/context/ranges-context'
 import { Threads } from '@/features/review-panel-new/context/threads-context'
 import { Threads } from '@/features/review-panel-new/context/threads-context'
+import { isSelectionWithinOp } from '@/features/review-panel-new/utils/is-selection-within-op'
 
 
 type RangesData = {
 type RangesData = {
   ranges: Ranges
   ranges: Ranges
@@ -20,6 +29,7 @@ type RangesData = {
 }
 }
 
 
 const updateRangesEffect = StateEffect.define<RangesData>()
 const updateRangesEffect = StateEffect.define<RangesData>()
+const highlightRangesEffect = StateEffect.define<AnyOperation | undefined>()
 
 
 export const updateRanges = (data: RangesData): TransactionSpec => {
 export const updateRanges = (data: RangesData): TransactionSpec => {
   return {
   return {
@@ -27,6 +37,12 @@ export const updateRanges = (data: RangesData): TransactionSpec => {
   }
   }
 }
 }
 
 
+export const highlightRanges = (op?: AnyOperation): TransactionSpec => {
+  return {
+    effects: highlightRangesEffect.of(op),
+  }
+}
+
 type Options = {
 type Options = {
   currentDoc: DocumentContainer
   currentDoc: DocumentContainer
   loadingThreads?: boolean
   loadingThreads?: boolean
@@ -89,6 +105,68 @@ export const ranges = ({ ranges, threads }: Options) => {
       }
       }
     ),
     ),
 
 
+    // draw highlight decorations
+    ViewPlugin.define<
+      PluginValue & {
+        decorations: DecorationSet
+      }
+    >(
+      () => {
+        return {
+          decorations: Decoration.none,
+          update(update) {
+            for (const transaction of update.transactions) {
+              this.decorations = this.decorations.map(transaction.changes)
+
+              for (const effect of transaction.effects) {
+                if (effect.is(highlightRangesEffect)) {
+                  this.decorations = buildHighlightDecorations(
+                    'ol-cm-change-highlight',
+                    effect.value
+                  )
+                }
+              }
+            }
+          },
+        }
+      },
+      {
+        decorations: value => value.decorations,
+      }
+    ),
+
+    // draw focus decorations
+    ViewPlugin.define<
+      PluginValue & {
+        decorations: DecorationSet
+      }
+    >(
+      () => {
+        return {
+          decorations: Decoration.none,
+          update(update) {
+            this.decorations = Decoration.none
+
+            if (!ranges) {
+              return
+            }
+
+            for (const range of [...ranges.changes, ...ranges.comments]) {
+              if (isSelectionWithinOp(range.op, update.state.selection.main)) {
+                this.decorations = buildHighlightDecorations(
+                  'ol-cm-change-focus',
+                  range.op
+                )
+              }
+            }
+          },
+        }
+      },
+      {
+        decorations: value => value.decorations,
+      }
+    ),
+
     // styles for change decorations
     // styles for change decorations
     trackChangesTheme,
     trackChangesTheme,
   ]
   ]
@@ -115,6 +193,29 @@ const buildChangeDecorations = (data: RangesData) => {
   return Decoration.set(decorations, true)
   return Decoration.set(decorations, true)
 }
 }
 
 
+const buildHighlightDecorations = (className: string, op?: AnyOperation) => {
+  if (!op) {
+    return Decoration.none
+  }
+
+  if (isDeleteOperation(op)) {
+    // nothing to highlight for deletions (for now)
+    // TODO: add highlight when delete indicator is done
+    return Decoration.none
+  }
+
+  const opFrom = op.p
+  const opLength = isInsertOperation(op) ? op.i.length : op.c.length
+  const opType = isInsertOperation(op) ? 'i' : 'c'
+
+  return Decoration.set(
+    Decoration.mark({
+      class: `${className} ${className}-${opType}`,
+    }).range(opFrom, opFrom + opLength),
+    true
+  )
+}
+
 class ChangeDeletedWidget extends WidgetType {
 class ChangeDeletedWidget extends WidgetType {
   constructor(public change: Change<DeleteOperation>) {
   constructor(public change: Change<DeleteOperation>) {
     super()
     super()
@@ -202,4 +303,28 @@ const trackChangesTheme = EditorView.baseTheme({
     borderLeft: '2px dotted #c5060b',
     borderLeft: '2px dotted #c5060b',
     marginLeft: '-1px',
     marginLeft: '-1px',
   },
   },
+  '&light .ol-cm-change-highlight-i': {
+    backgroundColor: '#b8dbc899',
+  },
+  '&dark .ol-cm-change-highlight-i': {
+    backgroundColor: '#b8dbc899',
+  },
+  '&light .ol-cm-change-highlight-c': {
+    backgroundColor: '#fcc4837d',
+  },
+  '&dark .ol-cm-change-highlight-c': {
+    backgroundColor: '#fcc4837d',
+  },
+  '&light .ol-cm-change-focus-i': {
+    backgroundColor: '#B8DBC8',
+  },
+  '&dark .ol-cm-change-focus-i': {
+    backgroundColor: '#B8DBC8',
+  },
+  '&light .ol-cm-change-focus-c': {
+    backgroundColor: '#FCC483',
+  },
+  '&dark .ol-cm-change-focus-c': {
+    backgroundColor: '#FCC483',
+  },
 })
 })