-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Visual revisions: Add a code diff view inside the editor #80314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
485f29e
Editor: Add code diff renderer for revisions
priethor 465c76e
Editor: Enable code view in revisions
priethor 42228e9
Editor: Remove unused MoreMenu disabled prop
priethor 9b79bfe
Editor: Handle large and paginated revision code diffs
priethor a0985c2
Editor: Preserve focus while revisions load
priethor 391c712
Editor: Polish revision code diff styling
priethor bcd3d61
Test revision components without module mocks
priethor cd34da9
Editor: Remove redundant revision diff code
priethor f6f5ea7
Editor: Handle failed revision diff requests
priethor 6f7f578
Editor: Simplify revision slider focus handling
priethor 15de62b
Editor: Fix duplicate changelog section
priethor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { default as RevisionsHeader } from './revisions-header'; | ||
| export { default as RevisionsCanvas } from './revisions-canvas'; | ||
| export { default as RevisionsCodeDiff } from './revisions-code-diff'; |
236 changes: 236 additions & 0 deletions
236
packages/editor/src/components/post-revisions-preview/revisions-code-diff.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { diffLines } from 'diff'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { Spinner } from '@wordpress/components'; | ||
| import { useSelect } from '@wordpress/data'; | ||
| import { useMemo } from '@wordpress/element'; | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { VisuallyHidden } from '@wordpress/ui'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { store as editorStore } from '../../store'; | ||
| import { unlock } from '../../lock-unlock'; | ||
|
|
||
| const MAX_DIFF_EDIT_LENGTH = 1000; | ||
| const DIFF_TIMEOUT = 100; | ||
|
|
||
| /** | ||
| * Diff parts often end in a newline. Remove the trailing empty item so it is | ||
| * not rendered as another source line. | ||
| * | ||
| * @param {string} value Diff part value. | ||
| * @return {string[]} Lines in the diff part. | ||
| */ | ||
| function splitLines( value ) { | ||
| const lines = value.split( '\n' ); | ||
| if ( lines[ lines.length - 1 ] === '' ) { | ||
| lines.pop(); | ||
| } | ||
| return lines; | ||
| } | ||
|
|
||
| /** | ||
| * Creates the rows shown in the code diff and adds line numbers for both | ||
| * revisions. | ||
| * | ||
| * @param {string} previousContent Previous revision content. | ||
| * @param {string} currentContent Selected revision content. | ||
| * @param {boolean} showDiff Whether changes should be highlighted. | ||
| * @return {Array<Object>} Code-diff rows. | ||
| */ | ||
| export function getCodeDiffRows( previousContent, currentContent, showDiff ) { | ||
| let parts = [ { value: currentContent } ]; | ||
| if ( showDiff ) { | ||
| parts = diffLines( previousContent, currentContent, { | ||
| maxEditLength: MAX_DIFF_EDIT_LENGTH, | ||
| timeout: DIFF_TIMEOUT, | ||
| } ); | ||
|
|
||
| // Line diffing can be quadratic for unrelated revisions. If it exceeds | ||
| // either limit, mark every old and new line instead. | ||
| if ( ! parts ) { | ||
| parts = [ | ||
| { value: previousContent, removed: true }, | ||
| { value: currentContent, added: true }, | ||
| ]; | ||
| } | ||
| } | ||
| let previousLineNumber = 1; | ||
| let currentLineNumber = 1; | ||
|
|
||
| return parts.flatMap( ( part ) => { | ||
| let status = 'unchanged'; | ||
| if ( part.added ) { | ||
| status = 'added'; | ||
| } else if ( part.removed ) { | ||
| status = 'removed'; | ||
| } | ||
|
|
||
| return splitLines( part.value ).map( ( value ) => { | ||
| const row = { | ||
| value, | ||
| status, | ||
| previousLineNumber: null, | ||
| currentLineNumber: null, | ||
| }; | ||
|
|
||
| if ( status !== 'added' && showDiff ) { | ||
| row.previousLineNumber = previousLineNumber++; | ||
| } | ||
| if ( status !== 'removed' ) { | ||
| row.currentLineNumber = currentLineNumber++; | ||
| } | ||
|
|
||
| return row; | ||
| } ); | ||
| } ); | ||
| } | ||
|
|
||
| export default function ConnectedRevisionsCodeDiff() { | ||
| const revisionDiff = useSelect( ( select ) => { | ||
| const editorSelectors = select( editorStore ); | ||
| const { | ||
| getCurrentRevision, | ||
| getPreviousRevision, | ||
| getRevisionPage, | ||
| getRevisionsPerPage, | ||
| isShowingRevisionDiff, | ||
| } = unlock( editorSelectors ); | ||
| const _previousRevision = getPreviousRevision(); | ||
| const _showDiff = isShowingRevisionDiff(); | ||
| const totalPages = | ||
| Math.ceil( | ||
| editorSelectors.getCurrentPostRevisionsCount() / | ||
| getRevisionsPerPage() | ||
| ) || 1; | ||
|
|
||
| return { | ||
| revision: getCurrentRevision(), | ||
| previousRevision: _previousRevision, | ||
| showDiff: _showDiff, | ||
| isPreviousRevisionLoading: | ||
|
priethor marked this conversation as resolved.
Outdated
|
||
| _showDiff && | ||
| _previousRevision === null && | ||
| getRevisionPage() < totalPages, | ||
| }; | ||
| }, [] ); | ||
|
|
||
| return <RevisionsCodeDiff { ...revisionDiff } />; | ||
| } | ||
|
|
||
| /** | ||
| * Shows the selected revision's raw block markup as a read-only diff. | ||
| * | ||
| * @param {Object} props Component props. | ||
| * @param {Object} props.revision Selected revision. | ||
| * @param {Object|null} props.previousRevision Previous revision. | ||
| * @param {boolean} props.showDiff Whether to show changes. | ||
| * @param {boolean} props.isPreviousRevisionLoading Whether the previous revision is loading. | ||
| * @return {React.JSX.Element} The revision code diff. | ||
| */ | ||
| export function RevisionsCodeDiff( { | ||
| revision, | ||
| previousRevision, | ||
| showDiff, | ||
| isPreviousRevisionLoading, | ||
| } ) { | ||
| const rows = useMemo( () => { | ||
| if ( ! revision || isPreviousRevisionLoading ) { | ||
| return []; | ||
| } | ||
|
|
||
| return getCodeDiffRows( | ||
| previousRevision?.content?.raw ?? '', | ||
| revision.content?.raw ?? '', | ||
| showDiff | ||
| ); | ||
| }, [ revision, previousRevision, showDiff, isPreviousRevisionLoading ] ); | ||
|
|
||
| if ( ! revision || isPreviousRevisionLoading ) { | ||
| return ( | ||
| <div className="editor-revisions-canvas__loading"> | ||
| <Spinner /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const label = showDiff ? __( 'Code changes' ) : __( 'Revision code' ); | ||
|
|
||
| return ( | ||
| <div | ||
| className="editor-revisions-code-diff" | ||
| role="region" | ||
| aria-label={ label } | ||
| tabIndex={ 0 } | ||
| > | ||
| { rows.length ? ( | ||
| <table className="editor-revisions-code-diff__table"> | ||
| <VisuallyHidden render={ <caption /> }> | ||
| { label } | ||
| </VisuallyHidden> | ||
| <VisuallyHidden render={ <thead /> }> | ||
| <tr> | ||
| { showDiff && <th>{ __( 'Previous line' ) }</th> } | ||
| <th>{ __( 'Current line' ) }</th> | ||
| { showDiff && <th>{ __( 'Change' ) }</th> } | ||
| <th>{ __( 'Code' ) }</th> | ||
| </tr> | ||
| </VisuallyHidden> | ||
| <tbody> | ||
| { rows.map( ( row, index ) => { | ||
| let marker = ''; | ||
| let statusLabel = __( 'Unchanged' ); | ||
| if ( row.status === 'added' ) { | ||
| marker = '+'; | ||
| statusLabel = __( 'Added' ); | ||
| } else if ( row.status === 'removed' ) { | ||
| marker = '−'; | ||
| statusLabel = __( 'Removed' ); | ||
| } | ||
|
|
||
| return ( | ||
| <tr | ||
| key={ index } | ||
| className={ `editor-revisions-code-diff__line is-${ row.status }` } | ||
| > | ||
| { showDiff && ( | ||
| <td className="editor-revisions-code-diff__line-number is-previous"> | ||
| { row.previousLineNumber } | ||
| </td> | ||
| ) } | ||
| <td className="editor-revisions-code-diff__line-number is-current"> | ||
| { row.currentLineNumber } | ||
| </td> | ||
| { showDiff && ( | ||
| <td className="editor-revisions-code-diff__marker"> | ||
| <VisuallyHidden> | ||
| { statusLabel } | ||
| </VisuallyHidden> | ||
| <span aria-hidden="true"> | ||
| { marker } | ||
| </span> | ||
| </td> | ||
| ) } | ||
| <td className="editor-revisions-code-diff__code"> | ||
| <code>{ row.value }</code> | ||
| </td> | ||
| </tr> | ||
| ); | ||
| } ) } | ||
| </tbody> | ||
| </table> | ||
| ) : ( | ||
| <p className="editor-revisions-code-diff__empty"> | ||
| { __( 'This revision is empty.' ) } | ||
| </p> | ||
| ) } | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.