diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx
index 59d262a4ae..434c2d9c5f 100644
--- a/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx
+++ b/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 { EditorView } from '@codemirror/view'
import classNames from 'classnames'
+import { highlightRanges } from '@/features/source-editor/extensions/ranges'
export const ReviewPanelEntry: FC<{
position: number
@@ -40,6 +41,8 @@ export const ReviewPanelEntry: FC<{
setFocused(false)}
+ onMouseEnter={() => view.dispatch(highlightRanges(op))}
+ onMouseLeave={() => view.dispatch(highlightRanges())}
role="button"
tabIndex={position + 1}
className={classNames(
diff --git a/services/web/frontend/js/features/source-editor/extensions/ranges.ts b/services/web/frontend/js/features/source-editor/extensions/ranges.ts
index 7b952b45cb..a3caf2b908 100644
--- a/services/web/frontend/js/features/source-editor/extensions/ranges.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/ranges.ts
@@ -7,12 +7,21 @@ import {
ViewPlugin,
WidgetType,
} from '@codemirror/view'
-import { Change, DeleteOperation } from '../../../../../types/change'
+import {
+ AnyOperation,
+ Change,
+ DeleteOperation,
+} from '../../../../../types/change'
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 { Ranges } from '@/features/review-panel-new/context/ranges-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 = {
ranges: Ranges
@@ -20,6 +29,7 @@ type RangesData = {
}
const updateRangesEffect = StateEffect.define
()
+const highlightRangesEffect = StateEffect.define()
export const updateRanges = (data: RangesData): TransactionSpec => {
return {
@@ -27,6 +37,12 @@ export const updateRanges = (data: RangesData): TransactionSpec => {
}
}
+export const highlightRanges = (op?: AnyOperation): TransactionSpec => {
+ return {
+ effects: highlightRangesEffect.of(op),
+ }
+}
+
type Options = {
currentDoc: DocumentContainer
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
trackChangesTheme,
]
@@ -115,6 +193,29 @@ const buildChangeDecorations = (data: RangesData) => {
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 {
constructor(public change: Change) {
super()
@@ -202,4 +303,28 @@ const trackChangesTheme = EditorView.baseTheme({
borderLeft: '2px dotted #c5060b',
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',
+ },
})