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

Highlight and focus ranges in editor for new review panel

GitOrigin-RevId: 4fef31a8c9e6dc974519d925c7478665e0c8cc29
This commit is contained in:
David 2024-09-19 11:08:20 +01:00 committed by Copybot
parent 8a0bed71e1
commit 22e4c2de2c
2 changed files with 130 additions and 2 deletions

View file

@ -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<{
<div
onFocus={focusHandler}
onBlur={() => setFocused(false)}
onMouseEnter={() => view.dispatch(highlightRanges(op))}
onMouseLeave={() => view.dispatch(highlightRanges())}
role="button"
tabIndex={position + 1}
className={classNames(

View file

@ -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<RangesData>()
const highlightRangesEffect = StateEffect.define<AnyOperation | undefined>()
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<DeleteOperation>) {
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',
},
})