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', + }, })