react-diff-view

repository·master·Indexed 21 days ago

https://github.com/otakustay/react-diff-view

A React component for rendering git unified diff output. It supports split and unified views, custom decorations, widgets for inline comments or warnings, and a token system for syntax highlighting and inline edit marking. Includes utilities like parseDiff for converting raw git diff text into structured data and functions for calculating line numbers and managing hunks.

Tokens
17.4K
Snippets
58
Records
75
Agent score
76%

What's inside react-diff-view

  1. Add widgets to changes

    master

    Widgets are React elements bound to specific changes, useful for features like code commenting or inline warnings.

    How they work

    • You provide a widgets object to the Diff component where keys are change keys (computed via getChangeKey) and values are React elements.
    • In split view: A widget renders on the corresponding side if the change is an addition or deletion. For other change types, it renders across the entire row.
    • Limitation: Each change can only render one widget. If multiple entries exist for the same change, only the first is rendered.
    import {parseDiff, getChangeKey, Diff} from 'react-diff-view';
    
    const getWidgets = hunks => {
        const changes = hunks.reduce((result, {changes}) => [...result, ...changes], []);
        const longLines = changes.filter(({content}) => content.length > 120);
        return longLines.reduce(
            (widgets, change) => {
                const changeKey = getChangeKey(change);
    
                return {
                    ...widgets,
                    [changeKey]: <span className="error">Line too long</span>,
                };
            },
            {}
        );
    };
    
    const App = ({diffText}) => {
        const files = parseDiff(diffText);
    
        return (
            <div>
                {files.map(({hunks}, i) => (
                    <Diff 
                        key={i} 
                        hunks={hunks} 
                        widgets={getWidgets(hunks)} 
                        viewType="split" 
                    />
                ))}
            </div>
        );
    };
  2. Add decorations around hunks

    master

    Decorations allow you to render custom content around Hunk components. To use them, pass a Decoration element within the Diff component's children function.

    Decoration accepts a children prop which can be:

    1. A single element: Rendered across the entire row.
    2. An array of two elements: The first element is rendered in the gutter position, and the second is rendered in the code position.
    import {flatMap} from 'lodash';
    import {Diff, Hunk, Decoration} from 'react-diff-view';
    
    const renderHunk = hunk => [
        <Decoration key={'decoration-' + hunk.content}>
            <SmileFace />,
            <span>{hunk.content}</span>
        </Decoration>,
        <Hunk key={'hunk-' + hunk.content} hunk={hunk} />
    ];
    
    const DiffFile = ({diffType, hunks}) => (
        <Diff viewType="split" diffType={diffType}>
            {flatMap(hunks, renderHunk)}
        </Diff>
    );
  3. Offload tokenization to a Web Worker with `withTokenizeWorker`

    master

    The withTokenizeWorker HOC enables syntax highlighting/tokenization to run in a Web Worker. This prevents the main browser thread from becoming unresponsive when processing large diffs.

    1. Implement the Worker

    Your worker must implement a specific communication protocol. It listens for messages and must return the same id provided in the request.

    Request Format:

    {
        "type": "tokenize",
        "id": 112,
        "payload": {
            "language": "jsx",
            "oldSource": "...",
            "hunks": [...]
        }
    }

    Response Format (Success):

    {
        "id": 112,
        "payload": {
            "success": true,
            "tokens": [...]
        }
    }

    Response Format (Failure):

    {
        "id": 112,
        "payload": {
            "success": false,
            "reason": "Error message"
        }
    }

    2. Configure the HOC

    Parameters:

    • {Worker} worker: An instance of your implemented Web Worker.
    • {Object} options:
      • {Function} mapPayload: (payload, props) => newPayload. Allows adding custom properties to the payload sent to the worker.
      • {Function} shouldTokenize: (currentPayload, prevPayload) => boolean. Determines if a new tokenization task should be triggered. The default logic handles oldSource changes and identity checks for hunks to avoid redundant work.

    Props passed to the wrapped component:

    • {Array} tokens: The tokenized data (pass this to the Diff component).
    • {any} tokenizationFailReason: The reason if tokenization fails.

    Note: When using highlighting, you must provide the language prop to the enhanced component.

    // Example Worker Implementation
    import {tokenize, markEdits, markWord} from 'react-diff-view/tokenize';
    import refractor from 'refractor';
    
    self.addEventListener('message', ({data: {id, payload}}) => {
        const {hunks, oldSource, language} = payload;
        const options = {
            highlight: language !== 'text',
            refractor: refractor,
            language: language,
            oldSource: oldSource,
            enhancers: [
                markWord('\r', 'carriage-return', '␍'),
                markWord('\t', 'tab', '→'),
                markEdits(hunks, {type: 'block'})
            ]
        };
    
        try {
            const tokens = tokenize(hunks, options);
            self.postMessage({ id, payload: { success: true, tokens } });
        } catch (ex) {
            self.postMessage({ id, payload: { success: false, reason: ex.message } });
        }
    });
    
    // --- Usage in React ---
    
    import {withTokenizeWorker, Diff, Hunk} from 'react-diff-view';
    import TokenizeWorker from './tokenize.worker.js';
    
    const worker = new TokenizeWorker();
    const EnhancedDiff = withTokenizeWorker(worker)(Diff);
    
    // Usage:
    <EnhancedDiff language="jsx" oldSource={...} hunks={...} />
  4. Customize styles with CSS variables

    master

    To customize the appearance of the diff, you can import the default stylesheet from react-diff-view/style/index.css.

    react-diff-view uses CSS custom properties (variables) for its theme. You can override these in your global CSS or within a specific scope to change colors, fonts, and backgrounds for various diff elements like gutters, code blocks, and selections.

    /* Example of overriding CSS variables */
    :root {
        --diff-background-color: #f5f5f5;
        --diff-text-color: #333;
        --diff-selection-background-color: #b3d7ff;
        --diff-gutter-insert-background-color: #d6fedb;
        --diff-gutter-delete-background-color: #fadde0;
    }
  5. Handle gutter and code events with EventMap

    master

    The library provides an EventMap to bind custom handlers to DOM events on the gutter and the code area. These events are wrapped to provide ChangeEventArgs, which includes the side and the change data.

    ChangeEventArgs:

    • side?: The Side where the event occurred.
    • change: The ChangeData or null.

    EventMap structure: An EventMap is a partial mapping of standard DOM event names (e.g., onClick, onMouseEnter) to handler functions that accept (args: ChangeEventArgs, event: NativeEvent) => void.

  6. Use the token system for code highlighting and enhancements

    master

    The tokenize function is the primary way to parse and tokenize diffs for features like syntax highlighting, special word marking, and inline edits. It is recommended to run this in a Web Worker to avoid blocking the UI.

    tokenize(hunks, options) accepts:

    • highlight (boolean): Enable syntax highlighting.
    • refractor (Object): A refractor library instance (use version 3.x).
    • oldSource (string): The original source code (improves accuracy for multiline comments/templates).
    • language (string): The source code language (e.g., 'jsx').
    • enhancers (Function[]): A list of functions to enhance tokens.
    import refractor from 'refractor';
    
    const options = {
        highlight: true,
        refractor: refractor,
        oldSource: oldSource,
        language: 'jsx',
        enhancers: [
            markWord('\r', 'carriage-return'),
            markWord('\t', 'tab'),
            markEdits(hunks),
        ],
    };
    
    const tokens = tokenize(hunks, options);
  7. Customize component class names via props

    master

    You can inject custom class names into specific components using their respective props:

    Diff component

    • className: Adds a class to the root <table> element.

    Hunk component

    • className: The root <tbody> element.
    • lineClassName: Each change's <tr> element.
    • gutterClassName: The gutter <td> element in each row.
    • codeClassName: The code <td> element in each row.

    Decoration component

    • className: The root <tr> element.
    • gutterClassName: The gutter <td> element.
    • contentClassName: The content <td> element.
  8. Parse git diff text with parseDiff

    master

    Use the parseDiff function to convert raw git diff text into a structured array of files. For optimal results, generate your diff text using git diff -U1.

    Options

    • nearbySequences: Determines how to handle nearby sequences of deletions and additions. Setting this to "zip" will interleave deletions and additions to provide a better visual experience in split view, rather than showing them as separate blocks.
    // Example usage of parseDiff
    const files = parseDiff(diffText, { nearbySequences: 'zip' });
  9. Handle gutter and code events

    master

    The Hunk component allows you to attach custom event handlers to the gutter and code cells using the gutterEvents and codeEvents props.

    Both props accept an object where keys are DOM event names (e.g., onClick) and values are callback functions. Each callback receives an object containing:

    • change: The change object associated with the event.
    • side: The side of the diff ("old" or "new"). This is undefined in unified mode.
    import {useState, useCallback, useMemo} from 'react';
    
    function File({hunks, diffType}) {
        const [selectedChanges, setSelectedChanges] = useState([]);
        
        const selectChange = useCallback(
            ({change}) => {
                const toggle = selectedChanges => {
                    const index = selectedChanges.indexOf(change);
                    if (index >= 0) {
                        return [
                            ...selectedChanges.slice(0, index),
                            ...selectedChanges.slice(index + 1),
                        ];
                    }
                    return [...selectedChanges, change];
                };
                setSelectedChanges(toggle);
            },
            [selectedChanges]
        );
    
        const diffProps = useMemo(
            () => ({
                gutterEvents: {onClick: selectChange},
                codeEvents: {onClick: selectChange},
            }),
            [selectChange]
        );
    
        return (
            <Diff viewType="split" diffType={diffType} hunks={hunks} {...diffProps}>
                {hunks => hunks.map(hunk => <Hunk key={hunk.content} hunk={hunk} />)}
            </Diff>
        );
    }
  10. Expand collapsed blocks manually with `withSourceExpansion`

    master

    The withSourceExpansion HOC provides a mechanism to programmatically expand collapsed text blocks in a diff.

    Props passed to the wrapped component:

    • {Function} onExpandRange: A callback to expand a specific range of text. It accepts two arguments: ({number} startLineNumber, {number} endLineNumber).

    Requirement:

    • You must provide the oldSource prop to the enhanced component.

    This HOC is useful for creating custom UI elements (like a "Click to expand" button) that trigger the expansion of specific line ranges.

    import {Diff, Decoration, Hunk, withSourceExpansion} from 'react-diff-view';
    
    const UnfoldCollapsed = ({previousHunk, currentHunk, onClick}) => {
        const start = previousHunk ? previousHunk.oldStart + previousHunk.oldLines : 1;
        const end = currentHunk.oldStart - 1;
    
        return (
            <div onClick={() => onClick(start, end)}>
                Click to expand
            </div>
        );
    };
    
    const DiffView = ({hunks, onExpandRange}) => {
        const renderHunk = (children, hunk) => {
            const previousElement = children[children.length - 1];
            const decorationElement = (
                <UnfoldCollapsed
                    key={'decoration-' + hunk.content}
                    previousHunk={previousElement && previousElement.props.hunk}
                    currentHunk={hunk}
                    onClick={onExpandRange}
                />
            );
            children.push(decorationElement);
    
            const hunkElement = (
                <Hunk
                    key={'hunk-' + hunk.content}
                    hunk={hunk}
                />
            );
            children.push(hunkElement);
    
            return children;
        };
    
        return (
            <Diff hunks={hunks} diffType="modify" viewType="split">
                {hunks => hunks.reduce(renderHunk, [])}
            </Diff>
        );
    };
    
    export default withSourceExpansion()(DiffView);
  11. Manage selection of changes with `withChangeSelect`

    master

    The withChangeSelect HOC manages the selection state of changes within a diff. It allows users to select one or multiple changes by interacting with the diff components.

    Options:

    • {Object} options: Configuration object.
      • {boolean} options.multiple = false: If true, enables selecting multiple changes simultaneously.

    Props passed to the wrapped component:

    • {string[]} selectedChanges: An array of keys representing the currently selected changes.
    • {Function} onToggleChangeSelection: A callback function used to toggle the selection of a specific change.

    To implement selection via clicking, pass onToggleChangeSelection to the codeEvents prop of the Hunk component.

    import {Diff, Hunk, withChangeSelect} from 'react-diff-view';
    
    const DiffView = ({hunks, selectedChanges, onToggleChangeSelection}) => {
        const codeEvents = {
            onClick: onToggleChangeSelection
        };
        const renderHunk = hunk => (
            <Hunk
                key={hunk.content}
                hunk={hunk}
                codeEvents={codeEvents}
            />
        );
    
        return (
            <Diff hunks={hunks} selectedChanges={selectedChanges}>
                {hunks => hunks.map(renderHunk)}
            </Diff>
        );
    };
    
    export default withChangeSelect({multiple: true})(DiffView);