You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[v2.1.0] – 2026-08-24
Added
Search across the file.:HexPairFind {bytes} looks through the
whole file a block at a time and lands the cursor on the byte it found,
turning the page on the way; a pattern is bytes (de ad be ef, deadbeef) and ? stands for any nibble. :HexPairFindText {string}
searches for the bytes of a string, :HexPairFindNext / :HexPairFindPrev repeat it either way and obey 'wrapscan', and
every match on the page is marked (HexPairFind) - including one that
straddles a page boundary, which is marked on both pages it touches. / could never do this: it searches the page on screen, which is
a window on the file. A search reads the file a megabyte at a time, and
from 16 MB up says how far it has got, since a scan of a large file takes
long enough to look like a hang; CTRL-C stops it, and nothing has been
changed by then. :HexPairDiffNext reports the same way.
Replacing what was found.:HexPairReplace {bytes} over the match
under the cursor, :HexPairReplaceAllInPage {pattern} / {bytes} over
every match on the page in view - the scope is in the name, because
everything here writes one page at a time and a file-wide replace would
be a different mechanism, not a bigger version of this one. Both edit
the page exactly as typing over the dump would, so nothing reaches the
file until :w does, and a replacement of a different length asks the
same question any other insertion does.
:HexPairDiff [file] marks every byte of the page that differs
from the same offset of another file (HexPairDiff) and says how many
differ; :HexPairDiffNext / :HexPairDiffPrev walk the whole file for
the next change - a run of differing bytes is one change however
long it is, so the jumps move between changes rather than through the
bytes of one, and backwards lands on a change's first byte as [c does
in a diff. :HexPairDiff! stops comparing and clears the marking
(<Plug>(HexPairDiffClear), and <Plug>(HexPairFindClear) for :HexPairFind!). The shell wrapper gains vimhexdiff FILE1 FILE2,
which opens both side by side, each marking what differs from the
other, cursors on the first difference and the windows scroll-bound -
and a page turn in either window takes the other with it, to the page
holding the same byte, since 'scrollbind' promises the two move
together and a page turn is the one kind of scrolling Vim cannot
follow on its own (g:hexpair_bind_pages).
Marks in the file: :HexPairMark {name}, :HexPairGoMark {name}, :HexPairMarks, :HexPairMarkDelete {name}, and the byte a mark
stands on underlined on the page (HexPairMark, g:hexpair_show_marks). All of them are reachable from a key: the
three that need a name ask for it and complete the names that exist
(<Plug>(HexPairMark), <Plug>(HexPairMarkDelete), <Plug>(HexPairGoMark)). Vim's own marks are positions in a buffer, and
a paged buffer holds a different part of the file from one page to the
next; these are absolute byte offsets kept per file, shared by every
view of it.
HexPairModified: the bytes edited and not yet written are marked
in both columns, so an edit in a dump no longer looks exactly like
everything around it (g:hexpair_show_modified turns it off). It links
to DiffChange rather than the closer-sounding DiffText, whose own
default is a red background with no foreground - black on red for
anyone with a light background.
The markings are drawn in the windowed text view too, one column
per byte where the dump gives a byte three: the bytes you edited, the
bytes that differ from the file being compared with, the matches of a
search, and the byte a mark stands on. The line break that ends a
text-view line is a byte of the page with no column of its own, and is
therefore the one byte never marked.
:HexPairModifiedNext / :HexPairModifiedPrev walk between the
runs of edited bytes the way :HexPairDiffNext walks changes: bytes
that touch are one edit, the cursor lands on the first byte of each,
and the message says which of how many. No scan of the file is needed -
turning a page needs an unmodified buffer or a bang that discards it,
so bytes edited and not yet written only ever exist on the page in
view.
hexpair.vimrc: the mappings the maintainer uses, shipped in the
repository and the release tarball so that a vimrc can source them
rather than copy them - runtime pack/*/start/hexpair/hexpair.vimrc,
one line that resolves on Linux, Windows and WSL alike because 'runtimepath' already names each platform's own per-user directory.
It never takes a key that is already mapped, and carries every option
and highlight group as a commented-out example.
Every command under a short name too: :HPFind, :HPToggle, :HPReplaceAllInPage - same arguments, same bang, same completion,
because :HexPair… is a lot to type at a : prompt. g:hexpair_short_commands = 0 leaves that namespace alone.
Prompting <Plug> targets for the commands that take something
typed: <Plug>(HexPairFind), <Plug>(HexPairFindText) and the three
mark ones above, each completing what it can - the same shape <Plug>(HexPairPageGoto) has had. The Visual-mode <Plug>(HexPairSelection) also puts the selection back when it has
reported on it: asking about a selection from the : line is what
ends Visual mode, and losing it to look at it is not a trade worth
making.
:HexPairGoOffset +N / -N steps from the byte the cursor is on,
crossing pages like a position does.
:HexPairOpen! abandons a modified buffer in the window, the bang
README had documented for a year and the command never had.
A data inspector.:HexPairInspect (<Plug>(HexPairInspect))
reads the bytes at the cursor as the numbers they could be: 8, 16, 32
and 64 bits wide, unsigned and signed, little- and big-endian, plus float32 and float64, with the byte itself also shown as a
character, in binary and in octal - and what the bytes would be as
text: UTF-8, UTF-16 and UTF-32, each saying what is wrong with the
bytes (an overlong sequence, a lone surrogate, a value past U+10FFFF)
rather than reporting a code point for something that is not one. The
bytes are the page's, as the buffer holds them — edits included — and
stop at its end, where the wider rows say how many are left rather than
reaching into a page that is not on screen.
:HexPairSelection (<Plug>(HexPairSelection), worth mapping in
Visual mode as well as Normal) says how many bytes a Visual selection
covers and which, 1-based like the banner, so the numbers can be typed
straight into :HexPairGoOffset. Asked from Visual mode it puts the
selection back and waits for a key, since Vim's own -- VISUAL -- is
drawn over the message line the moment it gets there. A blockwise
selection, whose bytes are not one run, leads with the count and says
how many lines and how many per line.
HexPairStatus() for 'statusline': hex 3/349 @0x50a01 (330241)
in the hex view, txt 3/349 @0x50a01 (330241) in the text view, and
an empty string in every buffer hexpair has not touched, so one
statusline serves both. It never walks the page — it is called on every
cursor movement — and marks a page with unwritten edits with a +.
g:hexpair_ruler (default 0): a ruler line between the banner and
the dump, numbering the byte columns — two digits over each hex byte,
the low nibble over each ASCII character. Like the banners it starts
with a " and therefore carries no bytes.
:HexPairPageGoto takes $ and +N/-N as well as a page
number, at the command line and at the <Plug> prompt alike — and
therefore in vimhex too: vimhex disk.img '$' opens the end of a
file without working out how many pages it has.
:HexPairSplit [page] and :HexPairVSplit [page]: a second view
of the same file in a new window, showing a page named the way :HexPairPageGoto names one and counted from the view you are in. The
two views share nothing but the file — each has its own buffer, page,
cursor and unwritten changes, and a :w in either patches only the
page that view holds, so one region can be read while another is
edited. Previously a second :HexPairOpen of the same file failed with E95, because the buffer's name was the file's alone; the second one
is now numbered (disk.img [hexpair page #2]).
g:hexpair_split_views (default 0): with it set, a plain :split, :vsplit or :tab split of a hex page becomes an independent view of
the same file too, opened on the same page and byte — and a split of
the text view stays a text view. Left off by default, because a page is
thousands of lines and looking at two parts of one page in two windows
is what :split is for everywhere else in Vim.
The page's own bytes are hashed when it is read and again before it
is patched, so a writer that changes bytes in place within the same
second — invisible to the file's size and modification time, which is
all a portable Vim can see — is caught rather than overwritten. It
costs one page read on either side (a page turn 13 ms → 26 ms, a
same-length write 129 ms → 144 ms at the default page size, both
independent of the size of the file). It also replaces the modification
time as the freshness test: what a write now asks is whether its own
page changed, not whether the file did, so a second view of the same
file — or any other process writing elsewhere in it — no longer locks a
write out. A file whose length changed is still refused outright,
since that moves every page after the change.
Changed
A page is scanned with whole-page regexes instead of a walk over its
lines, which is most of what a write used to cost. On the default
128 KiB page, measured on the author's machine: a same-length :w
goes from 571 ms to 129 ms, a toggle to the text view from 539 ms to
83 ms, and :HexPairPages from 274 ms to under a millisecond. Nothing
about the payload rule changed — the per-line rule is still the
reference the whole-page pass is tested against, on a page of
thousands of lines. One consequence is visible: the bytes before the
cursor's line are counted from the digits of the preceding lines as
one run rather than rounding each line down on its own, which is what xxd -r -p does when the page is written back, so a heavily edited
page with an odd number of digits on some line reports the byte the
write would actually produce.
A page of a file the user cannot write opens 'readonly', so :w
refuses it with Vim's own E45 the moment the page is opened, instead
of converting the page and surfacing a Permission denied from xxd
about a temporary file. :w! overrides it as everywhere else.
:HexPairGoOffset refuses a hex number written without the 0x
(ff), which used to be read as the decimal 0 and reported as "byte
positions start at 1, not 0" — a complaint about the wrong thing.
Fixed
The cursor in the gap between the hex and the ASCII column reported
the first byte of the NEXT line rather than the last of its own: the
pairs counted before it were the whole line's. :HexPairPages, the
byte a write puts the cursor back on and the statusline all read that.
g:hexpair_debug does something again. The documented
position-mapping trace had lost every one of its call sites in the
v2.0.0 rewrite, while README.md and the plugin header went on
describing it. It traces the transitions — a page load, the ++bin
reload, a byte turned into a position and a position turned back into
a byte in either view, and what a write found.
The plugin runs on the Vim it claims again.count() over a
string (patch 8.0.0794) and Blob literals (8.1.0735) had made
everything past displaying a page fail on Vim 8.0, which the docs
promised for viewing, navigating, same-length writes and in-place
inserts. Verified against a Vim 8.0.0000 build: only the splice paths
are refused there, each with the readblob() gate message.
A cursor in the offset column reported a byte a few along from the
line's first: the offset's own digits were counted like any other hex
payload, so 00000210: read as four bytes of nothing. :HexPairPages, the byte a write puts the cursor back on and the data
inspector all read that. It is the line's first byte now, which is
what the column says.
Documentation that had stopped being true: a Vim requirement justified
by lambda expressions the plugin no longer uses, and a Limitations
section still describing undo across the conversion boundary and a
plugin that "operates on the whole buffer", which paging replaced.