import * as PropTypes from "prop-types"; import * as React from "react"; import SplitEditor from "./split"; const DiffMatchPatch = require("diff-match-patch"); import { IEditorProps } from "./types"; export interface IDiffEditorProps { cursorStart?: number; editorProps?: object; enableBasicAutocompletion?: boolean | string[]; enableLiveAutocompletion?: boolean | string[]; focus?: boolean; fontSize?: number; height?: string; highlightActiveLine?: boolean; maxLines?: number; minLines?: number; mode?: string; name?: string; className?: string; onLoad?: (editor: IEditorProps) => void; onChange?: (value: string[], event?: any) => void; onPaste?: (value: string) => void; onScroll?: (editor: IEditorProps) => void; orientation?: string; readOnly?: boolean; scrollMargin?: number[]; setOptions?: object; showGutter?: boolean; showPrintMargin?: boolean; splits?: number; style?: object; tabSize?: number; theme?: string; value?: string[]; width?: string; wrapEnabled?: boolean; } export interface IDiffEditorState { value: string[]; } export default class DiffComponent extends React.Component< IDiffEditorProps, IDiffEditorState > { public static propTypes: PropTypes.ValidationMap = { cursorStart: PropTypes.number, editorProps: PropTypes.object, enableBasicAutocompletion: PropTypes.bool, enableLiveAutocompletion: PropTypes.bool, focus: PropTypes.bool, fontSize: PropTypes.number, height: PropTypes.string, highlightActiveLine: PropTypes.bool, maxLines: PropTypes.number, minLines: PropTypes.number, mode: PropTypes.string, name: PropTypes.string, className: PropTypes.string, onLoad: PropTypes.func, onPaste: PropTypes.func, onScroll: PropTypes.func, onChange: PropTypes.func, orientation: PropTypes.string, readOnly: PropTypes.bool, scrollMargin: PropTypes.array, setOptions: PropTypes.object, showGutter: PropTypes.bool, showPrintMargin: PropTypes.bool, splits: PropTypes.number, style: PropTypes.object, tabSize: PropTypes.number, theme: PropTypes.string, value: PropTypes.array, width: PropTypes.string, wrapEnabled: PropTypes.bool }; public static defaultProps: Partial = { cursorStart: 1, editorProps: {}, enableBasicAutocompletion: false, enableLiveAutocompletion: false, focus: false, fontSize: 12, height: "500px", highlightActiveLine: true, maxLines: null, minLines: null, mode: "", name: "ace-editor", onLoad: null, onScroll: null, onPaste: null, onChange: null, orientation: "beside", readOnly: false, scrollMargin: [0, 0, 0, 0], setOptions: {}, showGutter: true, showPrintMargin: true, splits: 2, style: {}, tabSize: 4, theme: "github", value: ["", ""], width: "500px", wrapEnabled: true }; constructor(props: IDiffEditorProps) { super(props); this.state = { value: this.props.value }; this.onChange = this.onChange.bind(this); this.diff = this.diff.bind(this); } public componentDidUpdate() { const { value } = this.props; if (value !== this.state.value) { this.setState({ value }); } } public onChange(value: any) { this.setState({ value }); if (this.props.onChange) { this.props.onChange(value); } } public diff() { const dmp = new DiffMatchPatch(); const lhString = this.state.value[0]; const rhString = this.state.value[1]; if (lhString.length === 0 && rhString.length === 0) { return []; } const diff = dmp.diff_main(lhString, rhString); dmp.diff_cleanupSemantic(diff); const diffedLines = this.generateDiffedLines(diff); const codeEditorSettings = this.setCodeMarkers(diffedLines); return codeEditorSettings; } public generateDiffedLines(diff: any) { const C = { DIFF_EQUAL: 0, DIFF_DELETE: -1, DIFF_INSERT: 1 }; const diffedLines = { left: [] as any[], right: [] as any[] }; const cursor = { left: 1, right: 1 }; diff.forEach((chunk: any) => { const chunkType = chunk[0]; const text = chunk[1]; let lines = text.split("\n").length - 1; // diff-match-patch sometimes returns empty strings at random if (text.length === 0) { return; } const firstChar = text[0]; const lastChar = text[text.length - 1]; let linesToHighlight = 0; switch (chunkType) { case C.DIFF_EQUAL: cursor.left += lines; cursor.right += lines; break; case C.DIFF_DELETE: // If the deletion starts with a newline, push the cursor down to that line if (firstChar === "\n") { cursor.left++; lines--; } linesToHighlight = lines; // If the deletion does not include a newline, highlight the same line on the right if (linesToHighlight === 0) { diffedLines.right.push({ startLine: cursor.right, endLine: cursor.right }); } // If the last character is a newline, we don't want to highlight that line if (lastChar === "\n") { linesToHighlight -= 1; } diffedLines.left.push({ startLine: cursor.left, endLine: cursor.left + linesToHighlight }); cursor.left += lines; break; case C.DIFF_INSERT: // If the insertion starts with a newline, push the cursor down to that line if (firstChar === "\n") { cursor.right++; lines--; } linesToHighlight = lines; // If the insertion does not include a newline, highlight the same line on the left if (linesToHighlight === 0) { diffedLines.left.push({ startLine: cursor.left, endLine: cursor.left }); } // If the last character is a newline, we don't want to highlight that line if (lastChar === "\n") { linesToHighlight -= 1; } diffedLines.right.push({ startLine: cursor.right, endLine: cursor.right + linesToHighlight }); cursor.right += lines; break; default: throw new Error("Diff type was not defined."); } }); return diffedLines; } // Receives a collection of line numbers and iterates through them to highlight appropriately // Returns an object that tells the render() method how to display the code editors public setCodeMarkers(diffedLines: any = { left: [], right: [] }) { const codeEditorSettings = []; const newMarkerSet = { left: [] as any[], right: [] as any[] }; for (let i = 0; i < diffedLines.left.length; i++) { const markerObj = { startRow: diffedLines.left[i].startLine - 1, endRow: diffedLines.left[i].endLine, type: "text", className: "codeMarker" }; newMarkerSet.left.push(markerObj); } for (let i = 0; i < diffedLines.right.length; i++) { const markerObj = { startRow: diffedLines.right[i].startLine - 1, endRow: diffedLines.right[i].endLine, type: "text", className: "codeMarker" }; newMarkerSet.right.push(markerObj); } codeEditorSettings[0] = newMarkerSet.left; codeEditorSettings[1] = newMarkerSet.right; return codeEditorSettings; } public render() { const markers = this.diff(); return ( ); } }