Skip to content

TextDiff

tsx
<TextDiff before={saved} after={draft} />
<TextDiff mode="editor" defaultBefore={saved} defaultAfter={draft} />
dart
TextDiff(before: saved, after: draft);
TextDiff(mode: DiffineMode.editor, defaultBefore: saved, defaultAfter: draft);

Modes

PropTypeDefaultWhat it decides
mode'viewer' | 'editor''viewer'Whether the two documents are read or written.
ArgumentTypeDefaultWhat it decides
modeDiffineModeDiffineMode.viewerWhether the two documents are read or written.

One component draws both.One widget draws both. editor lays a field over each pane, so the comparison is worked out again as somebody types into it; everything else — the rows, the tints, the marked words, the bands, the buttons, the search — is the same in both.

Several props are ignored in editor mode: view, alignLines, collapse, context, renderGutter and renderWidget, because a field cannot be a unified column, cannot be padded out with blanks somebody could type into, cannot hide the lines somebody is typing, and cannot have something of the application's own between its lines; and result, because a comparison worked out elsewhere is a comparison of documents nobody has typed into yet. Going the other way, readOnly, indentWithTab, spellCheck, defaultBefore, defaultAfter and onLanguageChange do nothing in viewer mode.

Several arguments are ignored in DiffineMode.editor: view, alignLines, collapse, context, renderGutter and renderWidget, because a field cannot be a unified column, cannot be padded out with blanks somebody could put the caret in, cannot hide the lines somebody is typing, and cannot have something of the application's own between its lines; and result, because a comparison worked out elsewhere is a comparison of documents nobody has typed into yet. Going the other way, readOnly, indentWithTab, defaultBefore, defaultAfter and onLanguageChanged do nothing in DiffineMode.viewer.

The documents

PropTypeDefaultWhat it is
beforestring | DiffineSource''The document on the left.
afterstring | DiffineSource''The document on the right.
defaultBeforestring | DiffineSource''What the left field starts with. Editor only.
defaultAfterstring | DiffineSource''What the right field starts with. Editor only.
onBeforeChange(value: string) => voidThe left document was typed into.
onAfterChange(value: string) => voidThe right document was typed into.
onDiff(result: DiffResult) => voidThe comparison, every time it is worked out again.
readOnlyboolean | 'before' | 'after'falseWhich side cannot be typed into. Editor only.
resultDiffResultA comparison already worked out. before and after are ignored. Viewer only.
diffDiffOptionsHow the two are compared.

DiffineSource is { content: string; label?: string }. The label is what the header calls that side; without one it is the word for it in the current locale.

A document nobody can type into is read from the props on every render. An editable one is the usual React pair instead: passing before or after makes that document the application's, passing defaultBefore or defaultAfter leaves it to the component, and which of the two it is, is decided on the first render. onBeforeChange and onAfterChange are called whichever of the two is holding it.

ArgumentTypeDefaultWhat it is
beforeString?The document on the left.
afterString?The document on the right.
beforeLabelString?What the header calls the left side.
afterLabelString?What it calls the right side.
defaultBeforeString?What the left field starts with. Editor only.
defaultAfterString?What the right field starts with. Editor only.
onBeforeChangedValueChanged<String>?The left document was typed into.
onAfterChangedValueChanged<String>?The right document was typed into.
onDiffValueChanged<DiffResult>?The comparison, every time it is worked out again.
readOnlyDiffineSide?Which side cannot be typed into. Editor only.
resultDiffResult?A comparison already worked out. before and after are ignored. Viewer only.
diffDiffOptionskDiffineDefaultsHow the two are compared.

The document and its name are two arguments rather than one value, because a String is already the whole document. Without a label the header writes the word for that side in the current locale.

A document nobody can type into is read from the widget on every build. An editable one is controlled or uncontrolled: passing before or after makes that document the application's, passing defaultBefore or defaultAfter leaves it to the widget, and which of the two it is, is decided on the first build. onBeforeChanged and onAfterChanged are called whichever of the two is holding it.

The view

PropTypeDefaultWhat it decides
view'split' | 'unified''split'One document either side, or one column with both.
lineNumbersbooleantrueWhether each line carries its number.
markersbooleantrueWhether a changed line carries a +, or ~.
wrapbooleanfalseWhether a long line wraps or runs off the side.
alignLinesbooleantrueWhether a line is held level with its counterpart.
collapsebooleanfalseWhether runs of unchanged lines far from a change are folded away.
contextnumber3How many unchanged lines are kept either side of a change.
connectorsbooleantrueWhether each change is drawn as a band between the panes.
applyChangesbooleanfalseWhether each change carries buttons for writing it into the other document.
syncScrollbooleantrueWhether scrolling one pane scrolls the other.
headerbooleantrueWhether each side is named above it.
navigationbooleantrueWhether the buttons for moving between changes are drawn.
searchbooleantrueWhether a reader can search a pane from inside the component.
summarybooleantrueWhether the bar under the view is drawn.
virtualizebooleantrueWhether only the lines a reader can see are drawn.
showInvisiblesbooleanfalseWhether the spaces and tabs inside a line are drawn.
languagestring'plain'What the documents are written in, so they are coloured as it.
defaultLanguagestring'plain'Which one to start on, when the component is to keep it.
onLanguageChange(language: string) => voidA language was chosen from the menu. Editor only.
languageLabelbooleanfalseWhether that language is drawn at the right end of the bar.
tabSizenumber4How wide a tab is drawn, in characters.
colorScheme'system' | 'light' | 'dark''system'Which palette to draw in.
fontDiffineFontThe typeface the documents are drawn in.
locale'en' | 'ko''en'The language of the component's own words.
stringsPartial<DiffineTextStrings>Words to use instead of the locale's.
highlightDiffineHighlightAn application's own highlighter, in place of language.
renderGutterDiffineRenderSomething of the application's own, in the gutter beside each line.
renderWidgetDiffineRenderSomething of the application's own, under each line.

collapse and context are the viewer's; an editor holds whole documents in its fields and folds nothing. applyChanges is the editor's, because applying a change means writing a document, and its buttons live in the column connectors draws. connectors and syncScroll are about the space between two panes, so both are ignored in the unified view. languageLabel draws the name of the language in viewer mode and the menu it was chosen from in editor mode; language, defaultLanguage and onLanguageChange are the usual pair for that choice.

Anything else the component is given goes straight to the element, so id, className, style and the aria-* attributes behave as they would on a <div>.

ArgumentTypeDefaultWhat it decides
viewDiffineViewDiffineView.splitOne document either side, or one column with both.
lineNumbersbooltrueWhether each line carries its number.
markersbooltrueWhether a changed line carries a +, or ~.
wrapboolfalseWhether a long line wraps or runs off the side.
alignLinesbooltrueWhether a line is held level with its counterpart.
collapseboolfalseWhether runs of unchanged lines far from a change are folded away.
contextint3How many unchanged lines are kept either side of a change.
connectorsbooltrueWhether each change is drawn as a band between the panes.
applyChangesboolfalseWhether each change carries buttons for writing it into the other document.
syncScrollbooltrueWhether scrolling one pane scrolls the other.
headerbooltrueWhether each side is named above it.
navigationbooltrueWhether the buttons for moving between changes are drawn.
searchbooltrueWhether a reader can search a pane from inside the widget.
summarybooltrueWhether the bar under the view is drawn.
showInvisiblesboolfalseWhether the spaces and tabs inside a line are drawn.
languageString?What the documents are written in, so they are coloured as it.
defaultLanguageString'plain'Which one to start on, when the widget is to keep it.
onLanguageChangedValueChanged<String>?A language was chosen from the menu. Editor only.
languageLabelbooltrueWhether that language is drawn at the right end of the bar.
colorSchemeDiffineColorScheme.systemWhich palette to draw in.
themeDiffineTheme?The whole palette, and the measurements with it.
fontDiffineFont?The typeface the documents are drawn in.
heightdouble?How tall the whole comparison is. double.infinity fills what holds it.
localeDiffineLocaleDiffineLocale.enThe language of the widget's own words.
stringsDiffineStrings?Words to use instead of the locale's.
highlightDiffineHighlight?An application's own highlighter, in place of language.
renderGutterDiffineRender?Something of the application's own, in the gutter beside each line.
renderWidgetDiffineRender?Something of the application's own, under each line.

collapse and context are the viewer's; an editor holds whole documents in its fields and folds nothing. applyChanges is the editor's, because applying a change means writing a document, and its buttons live in the column connectors draws. connectors and syncScroll are about the space between two panes, so both are ignored in the unified view. languageLabel draws the name of the language in the viewer and the menu it was chosen from in the editor; language, defaultLanguage and onLanguageChanged are the usual pair for that choice.

There is no virtualize: the rows are built as they are reached, always, because that is what a ListView is. renderWidget is what turns it off, and it does so on its own — what an application draws under a line can grow at any moment, and a row standing in for one of those would be standing in the wrong place.

tabSize is on the theme rather than here, because how wide a tab is drawn is a measurement and the measurements live together.

Typing

PropTypeDefaultWhat it decides
indentWithTabbooleanfalseWhether Tab types a tab instead of moving to the next control.
spellCheckbooleanfalseWhether the browser marks its own spelling mistakes.

Both are editor mode's. With indentWithTab on, Shift+Tab moves back a control and Escape hands the next Tab to the browser, so the field is never one a keyboard cannot leave.

ArgumentTypeDefaultWhat it decides
indentWithTabboolfalseWhether Tab types a tab instead of moving to the next control.

Editor only. With it on, Shift+Tab moves back a control and Escape hands the next Tab to the framework, so the field is never one a keyboard cannot leave.

There is no spellCheck. Spelling is the platform's on the platforms that have it and nothing at all on the ones that do not, and an argument that did nothing on half of them would be a promise the widget cannot keep.

Which change a reader is on

PropTypeDefaultWhat it is
selectednumberThe change being looked at, or -1.
defaultSelectednumber-1The one to start on.
onSelectedChange(selected: number, change: DiffChange | null) => voidA change was moved to.

selected is an index into changes. Passing it makes it the application's, in the usual React pair, and setting it scrolls the view exactly as pressing a button does. onSelectedChange is called whichever of the two is holding it.

ArgumentTypeDefaultWhat it is
selectedint?The change being looked at, or -1.
defaultSelectedint-1The one to start on.
onSelectedChangedvoid Function(int, DiffChange?)?A change was moved to.

selected is an index into changes. Passing it makes it the application's, and setting it scrolls the view exactly as pressing a button does. onSelectedChanged is called whichever of the two is holding it.

Searching a pane

Each pane is searched on its own: a button in the bar above it opens a bar of its own underneath it, and Ctrl+FCmd+F where that is the modifier — opens the one for the pane the keyboard is in. The two sides have two queries, two counts and two bars, and neither closes the other.

Matches are marked as the query is typed, the pane moves to the one being read, and Enter and Shift+Enter step through the rest. The three switches inside the box read the query as a case-sensitive one, as whole words only, and as a regular expression. Escape closes the bar.

The editor adds a row for replacing, which Ctrl+H opens together with the bar. The replacement is written as the text it is — $1 is a dollar and a one — and it goes in through the browser's own editing command, so Ctrl+Z takes it backthe field's own controller, so the platform's undo takes it back. A readOnly side is searched and not replaced in.

A pane whose search is open still draws only the lines a reader can see, so a match found on line nine thousand is scrolled to and drawn there. search={false}search: false turns the button and the shortcuts off together, which is what a pagescreen wants if those keys belong to something else on it.

Released under the MIT License