diff --git a/services/web/frontend/js/features/ide-react/components/modals/diff-viewer.tsx b/services/web/frontend/js/features/ide-react/components/modals/diff-viewer.tsx index d1cbe70273..6ab2d18500 100644 --- a/services/web/frontend/js/features/ide-react/components/modals/diff-viewer.tsx +++ b/services/web/frontend/js/features/ide-react/components/modals/diff-viewer.tsx @@ -1,5 +1,5 @@ import { memo, useCallback, useMemo } from 'react' -import { diffWordsWithSpace } from 'diff' +import { diffWordsWithSpace, structuredPatch } from 'diff' type DiffViewerProps = { baseContent: string @@ -7,36 +7,70 @@ type DiffViewerProps = { } // Renders an inline diff between two document versions using and -// tags. This mirrors the DOM-based rendering approach used by the workbench -// CodeDiff component (modules/workbench/frontend/js/components/code-diff.tsx). +// tags. Only affected lines are shown (plus one line of context above/below), +// with a separator between non-adjacent sections. // TODO: refactor both to share a common diff renderer from a shared location. function DiffViewer({ baseContent, targetContent }: DiffViewerProps) { - const changes = useMemo( - () => diffWordsWithSpace(baseContent, targetContent), - [baseContent, targetContent] - ) + const hunks = useMemo(() => { + const patch = structuredPatch('', '', baseContent, targetContent, '', '', { + context: 1, + }) + + if (patch.hunks.length === 0) { + return [diffWordsWithSpace(baseContent, targetContent)] + } + + return patch.hunks.map(hunk => { + const baseLines: string[] = [] + const targetLines: string[] = [] + + for (const line of hunk.lines) { + const content = line.slice(1) + if (line[0] === '-') { + baseLines.push(content) + } else if (line[0] === '+') { + targetLines.push(content) + } else if (line[0] === ' ') { + baseLines.push(content) + targetLines.push(content) + } + } + + return diffWordsWithSpace(baseLines.join('\n'), targetLines.join('\n')) + }) + }, [baseContent, targetContent]) const createDiff = useCallback( (element: HTMLDivElement) => { if (!element) return element.replaceChildren() - for (const change of changes) { - if (change.added) { - const ins = document.createElement('ins') - ins.textContent = change.value - element.append(ins) - } else if (change.removed) { - const del = document.createElement('del') - del.textContent = change.value - element.append(del) - } else { - const text = document.createTextNode(change.value) - element.append(text) + for (let i = 0; i < hunks.length; i++) { + if (i > 0) { + const separator = document.createElement('div') + separator.className = 'diff-separator' + separator.setAttribute('aria-hidden', 'true') + separator.textContent = '⋮' + element.append(separator) + } + + for (const change of hunks[i]) { + if (change.added) { + const ins = document.createElement('ins') + ins.textContent = change.value + element.append(ins) + } else if (change.removed) { + const del = document.createElement('del') + del.textContent = change.value + element.append(del) + } else { + const text = document.createTextNode(change.value) + element.append(text) + } } } }, - [changes] + [hunks] ) return
diff --git a/services/web/frontend/stylesheets/pages/editor/ide.scss b/services/web/frontend/stylesheets/pages/editor/ide.scss index efabeea1cb..1f3b0fc720 100644 --- a/services/web/frontend/stylesheets/pages/editor/ide.scss +++ b/services/web/frontend/stylesheets/pages/editor/ide.scss @@ -207,6 +207,13 @@ $editor-toggler-bg-dark-color: color.adjust( color: var(--red-50); text-decoration: line-through; } + + .diff-separator { + text-align: center; + color: var(--content-secondary); + padding: var(--spacing-01) 0; + user-select: none; + } } }