[v2.3.0] – 2026-09-02
Added
-
:HexPairUnhex, the way back out of a paged view. A file you opened
normally (vim file.md) and then switched to hex with<Leader>h/
:HexPairTogglecomes back to its ordinary, unpaged, non-binary view:
:HexPairUnhex(mapped to<Leader>Uinhexpair.vimrc) re-opens the
whole file with the'binary','fileencoding','fileformat'and the
other buffer options it had before the first toggle, with the cursor where
it was. The plugin takes a snapshot of those options the first time a
buffer toggles from plain, and hands them back rather than guessing. The
buffer is re-read from disk, not converted — a page is not the file, and
a page's unwritten edits would be lost, so without!the command refuses
and!discards them. What it cannot serve it refuses, each with its own
message: a file opened as hex by:HexPairOpenorvimhex, which may
have had no text view at all and whose file may be far too large to load
for one (it names the file to:editinstead); a view paged from content
with no file of its own; and one whose file has changed on disk since the
page was read. See:help :HexPairUnhex. -
:HexPairInspectnow says what the character is. Anamerow for
every code point you cannot see — the C0 and C1 controls,DEL, and the
space and format characters a hex editor meets constantly — ablockrow
naming the Unicode block, which answers what script even is this, and a
bomrow when the bytes at the cursor are a byte order mark, which the
UTF-8 rows cannot say becauseff feis not UTF-8. The byte order mark is
the one named character with noblockrow:U+FEFFsits in Arabic
Presentation Forms-B only because Unicode lays blocks out by contiguity
andFE70..FEFFcloses the BMP, and a format character belongs to no
script, so the row would only assert one where there is none. No full
character names: that needsUnicodeData.txt, which is a different order
of thing from a table of ranges. See:help hexpair-inspect-naming. -
:HexPairInspectworks in an ordinary buffer, with no hex view
anywhere near it - what is this character, is that a NBSP, does this file
start with a BOM. It says when the bytes it is showing are Vim's rather
than the file's, which is the moment'fileencoding'or'fileformat'
differs. See:help hexpair-inspect-anywhere. -
vimhex.cmdandvimhexdiff.cmd, thecmd.execounterparts of the
two shell functions inhexpair.bashrc- same names, same arguments, so
a hex view is opened the same way whichever shell you are standing in.
Put the plugin's own directory onPATHand updating the plugin updates
the commands;VIMHEX_VIMpicks the Vim,gvimincluded. -
Explorer context-menu entries, as a
vimhexsubmenu holding
gvimhex this, a separator, and gvimhexdiff select as left / select as
right. Two files take two clicks because Explorer runs a verb once per
selected file and there is no%2without a COM handler - so
vimhexdiff.cmdgrew/left FILEand/right FILE. They are
symmetric: either may be used first, each records its side and stops,
and whichever completes the pair opens the comparison and clears both
selections. Selecting the same side twice just overwrites it, and if the
other side's file has been moved or deleted since, only that selection is
cleared — the one just made is kept, so nothing has to be re-selected. -
gvimhex.cmdandgvimhexdiff.cmd, andgvimhex/gvimhexdiffin
hexpair.bashrc, opening gVim instead of console Vim by default - what a
double-click or a context-menu verb needs, since neither has a console
forvimto run in or a way to passVIMHEX_VIM. Each delegates to its
vimhex/vimhexdiffcounterpart rather than duplicating the argument
grammar. -
vimhex-contex-entry.add.regandvimhex-contex-entry.remove.reg,
ready-made versions of the registry snippets above - one import wires up
all three context-menu entries (pointing atgvimhex.cmd/
gvimhexdiff.cmd), the other removes them again. Nothing to edit for a
default install: the paths are written against
%USERPROFILE%\vimfiles\pack\plugins\start\hexpairasREG_EXPAND_SZ
values, the one registry string type whose%USERPROFILE%the shell
expands - a plainREG_SZwould have it look for a folder literally
named that. Installed elsewhere?make-context-entry-reg.pyregenerates
the pair for any path, which is also why these two files are generated
rather than hand-written:.regcan only express that type ashex(2):
plus UTF-16LE bytes. -
icons/hexpair-open.ico,-pick.ico,-with.ico, custom icons for
those three entries - Explorer has nothing to show for a verb whose
command is a.cmdfile otherwise. A V mark in Vim's own green, a0x
badge marking these as hexpair's, and on the diff pair a bigger badge of
two window panes (echoingvimhexdiff's ownvsplit) - blue left,
orange right, the side that entry represents shown at full colour and
the other dimmed, since text or an arrow stops reading reliably at
16px but colour still does. Generated byicons/build.py
(icons/design.py,icons/rasticon.py- a from-scratch PNG/ICO
encoder, no image library); only the.icofiles ship in the release
tarball, the generator is a development-only file. -
:HexPairDiffShow(<Plug>(HexPairDiffShow),<Leader>Din
hexpair.vimrc, Normal and Visual) says what the file being compared
with holds at the cursor - or over a whole selection - beside the bytes
here. The marking answers which bytes differ and stops there; this
answers what is over there instead, and in particular says when there
is nothing there because that file ends before this offset. Missing
bytes show as--, so an absent byte cannot be misread as a byte that
happens to be00. -
Files over 2 GiB on native Windows, which
xxdcannot reach at all
(see Fixed). Every operation past that offset — reading, searching,
comparing, overwriting, growing, shrinking,:w {file}— goes through
PowerShell, at the cost of one process start each. Shrinking is
actually cheaper there than anywhere else: neither Vim norxxdcan
shorten a file except by rewriting it, so a shrinking write copies the
whole file on every other platform, while.NET'sSetLengthtruncates
in place.README.md's Windows and the 2 GiB limit has the table of
what runs where. Every write past the limit reads back what it wrote
before reporting success;g:hexpair_verify_writes = 0opts out.
Changed
-
vimhexdiffopens maximized (:simalt ~x, guarded by
has('gui_running')) and setsshortmess+=F. Two hex views side by side
want the width anyway, and a narrow window was what made Vim stop for a
hit-enter prompt on each file: a long path plus the size makes the file
message longer than one line, which is what triggers it. -
:HexPairGoOffsettakes$for the file's last byte, the same
shorthand:HexPairPageGotoalready took for the last page - the two
prompts sit under neighbouring keys (<Leader>band<Leader>g), and
answering one in the other's language should not be a mistake. -
The Vim requirement is stated exactly, and proved. It is Vim 8.0 at
any patch level — the old wording asked for patch 8.0.0794, which has not
been true sincecount()over a string left the plugin — plus Vim
9.0.0795 with+num64for the three writes that change a file's length.
It is no longer a claim: CI builds Vim 8.0.0000 from source on every push
and runs the whole suite against it, where it passes in full because the
suite asks that Vim what it can do and checks that the rest is refused.
The floor had rotted twice while checking it was a manual ritual.
Fixed
- The splice named the wrong Vim patch, by a major release. Shortening a
file,:w {file}and a grow with more than half the file behind it need
readblob()with an offset and a size, which is patch 9.0.0795 —
plain whole-filereadblob()is 8.2.2343, and 8.2.4906, which the gate
asked for, is an unrelated MS-Windows patch. Any Vim between the two was
told it could do such a write and then failed withE118: Too many arguments for function: readblobpart way through the copy. The gate, the
message and the documented requirement all say 9.0.0795 now. xxdwas located only when a page was opened, so any other route
into the readers died withE121: Undefined variableinstead of a
message namingxxd. It is resolved on demand now, and a missingxxd
is reported as itself wherever it is first needed.- On native Windows, everything past 2 GiB was read and written at the
wrong offset.xxdkeeps its seek offset in a Clong—strtol()
intolong seekoff, thenfseek()— and alongis 32 bits on Windows,
which is LLP64. Worse,strtol()saturates: an offset past the limit
does not fail, it silently becomes2147483647andxxdreads, or
writes, a page from there. So a 120 GiB file compared with a 77 GiB one
showed bytes as matching on pages wholly past the shorter file's end —
both were being read at 2 GiB, where the shorter one does have data —
while the same hexpair on the same files was right under WSL, where a
longis 64 bits.xxd -randxxd -oshare the limit, so the page you
look at, the bytes a diff compares and the bytes a:wputs back were
all affected. Everything past that limit now goes through PowerShell —
see Windows and the 2 GiB limit inREADME.md. - A nonsensical
g:hexpair_bytes_per_linewas accepted. Zero passed
the "page size must be a multiple of it" check, because Vim answers
512 % 0with0rather than an error, and a negative one passed for
the same reason — both then failed later and obscurely, inxxd -c 0and
in every column sum on the page. So did anything over 256, which is
xxd's own ceiling for-cand made it exit with "invalid number of
columns". The check is now the range 1 to 256, and says so, along with
the one thing that does have to divide: the page size. g:hexpair_page_sizehad no upper bound. It is capped at
2147483647 bytes: a page's length reachesxxd -l(a Clong, 32-bit on
Windows) and PowerShell's[int], and a larger one would overflow both
silently rather than fail.- A file too large for this Vim to measure opened as an empty one.
getfsize()answers-2when the size does not fit in a Number — which
on a build without+num64is any file over 2 GiB — and-1when it
cannot see the file at all. Both were read as "no bytes", so such a file
opened as an empty view with the page count and every offset derived from
it silently meaningless. Both are now refused with a message naming the
cause; an actual empty file still opens and still says it is empty. - A
^Mat the end of every line of a page, on Windows.xxdopens a
dump in text mode there (xxd.c:BIN_ASSIGN(fpo = stdout, revert)for
the stream,BIN_WRITE(revert)for a named output file), so every dump
line reaches Vim CRLF-terminated - and the page loader read it through a
%!filter, which leaves what becomes of that CR to'fileformats'
auto-detection. That is a user option: withset fileformats=unixin a
vimrc nothing stripped it, and the whole dump was fringed with^M.
Cosmetic only - the CR sat past the ASCII column, in the region the
payload rules ignore, so it never reached the file - but it was on screen
on every page load and every page turn, and:HexPairRefreshcleared it
only because that path already went throughreadfile(), whose text mode
drops a CR before a NL whatever the options say. The loader now does the
same, as every otherxxdcall in the plugin already did. - A page read that fails no longer leaves the buffer claiming to be the
new page. The page state was assigned before the dump was read, so an
xxdthat failed part way left the buffer's bytes and its idea of which
page they are disagreeing - and a:wwould have patched the old page's
bytes in at the new page's offset. The read and its shape check now both
happen before anything about the buffer changes.