Patch files
A host that shows an editable .patch or .diff file can draw it the way the diff views draw a change, and list its files in a tree. Two pieces do that: patchLayoutOf reads the text, and decoratePatch applies the reading to a mounted editor. The editor stays fully editable. Saving the file is the host's.
Reading a patch
patchLayoutOf(text, limits?) is a pure function from @adecore/editor-react/models. It never throws, and it returns a PatchLayout:
lineshas one entry per line oftext.split('\n'), with arole, and for context, added and removed lines the one-basedoldandnewnumbers from the hunk headers (nullwhere the file has no such line).fileslists the file sections for a tree:oldPathandnewPath(nullfor/dev/null),status, the zero-basedfirstLineandlastLineof the section in the text, theaddedandremovedcounts and thehunks.truncatedsays the text went past a limit. A text that ends with a newline right at a limit is not truncated.- Paths lose
a/andb/only when thediff --gitline shows both (a/X b/Y); a patch made without prefixes keeps them, so a real top-level folderastays.
| Role | Lines |
|---|---|
file-header | diff --git, ---, +++, index, the mode, similarity, rename and copy lines |
hunk-header | @@ -a,b +c,d @@ section |
context, added, removed | The lines of a hunk. An empty line inside a hunk is context whose space an editor trimmed |
no-newline | \ No newline at end of file |
binary | Binary files a and b differ, and the data of a GIT binary patch |
other | Everything else: a mail header, a diffstat, a blank line, a line that belongs nowhere |
A file status is added, deleted, renamed, copied, modified or binary. Git patches and plain diff -u output both work, with timestamps after a tab, quoted paths with escapes and octal bytes, names with spaces, and CRLF line endings. A --- line followed by +++ starts a file section; inside a hunk that still expects lines, --- x is a removed line. The -- signature of a mailed patch is not a removed line.
An edited patch
A person who edits the patch breaks its counts. The reading goes on: a hunk whose lines no longer add up to its header gets consistent: false, a line typed below a complete hunk extends it and is numbered on, and a header that is cut short ends the hunk. The line numbers are the ones the hunk header and the lines above give, so they move with the edit and are not repaired.
Limits
limits has maxLines (50,000 by default) and maxCharacters (4,000,000); DEFAULT_PATCH_LIMITS holds both. Past either one, lines ends and truncated is true. The lines after the cut have no role and the last section ends where the reading stopped.
import { patchLayoutOf } from '@adecore/editor-react/models';
const layout = patchLayoutOf(text);
for (const file of layout.files) {
console.log(file.status, file.newPath, `+${file.added} -${file.removed}`);
}Drawing it
decoratePatch(editor, options?) styles a mounted editor that holds the patch text:
- Added lines get a solid bar and removed lines a striped one, with the tinted cell and number of the diff views, continuous over consecutive lines.
- The gutter shows the numbers from the hunk headers instead of the line numbers of the file, and none on header lines.
numbers: 'single'shows the old number on a removed line and the new one otherwise;'both'shows old and new side by side. - File headers sit on a raised surface, and hunk headers are tinted in the accent color.
- With
labels, a screen reader hearsRemoved, old line 8,Added, new line 12orOld line 3, new line 3with the line, once. A hook that fillslabelsfrom the editor namespace is below.
onLayout is called with the layout after the first read and after refresh(), and otherwise once the edits have rested for delay milliseconds (150 by default), so a file tree is not rebuilt per keystroke. It returns a PatchDecoration with the current layout, refresh() and dispose(), which takes the decorations off the editor again. The change marks are drawn in a layer of their own ('patch'), so dispose() leaves the marks a host set itself. A patch under immediateBelow characters (100,000 by default, measured on the last read) is read again on the microtask after an edit, so its decorations are right within a frame; a larger one is read again once the edits have rested for delay. A read is one pass over at most the limits. timer replaces setTimeout and clearTimeout for a test.
import { decoratePatch } from '@adecore/editor-react';
const decoration = decoratePatch(editor, {
numbers: 'both',
limits: { maxLines: 20_000 },
onLayout: (layout) => tree.setFiles(layout.files)
});In React, usePatchDecorations(editor, options?) does the same for as long as the editor stands, with the words of the editor namespace, and returns the layout, or null without an editor.
The decorations use line highlights, a line number provider, line labels and change marks of the editor, so a host that sets those for the same editor replaces them.