Skip to content

Releases: michal-ruzicka/hexpair

Release v2.3.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 02 Sep 18:46
v2.3.0
d9c0853

[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 /
    :HexPairToggle comes back to its ordinary, unpaged, non-binary view:
    :HexPairUnhex (mapped to <Leader>U in hexpair.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 :HexPairOpen or vimhex, 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 :edit instead); 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.

  • :HexPairInspect now says what the character is. A name row for
    every code point you cannot see — the C0 and C1 controls, DEL, and the
    space and format characters a hex editor meets constantly — a block row
    naming the Unicode block, which answers what script even is this, and a
    bom row when the bytes at the cursor are a byte order mark, which the
    UTF-8 rows cannot say because ff fe is not UTF-8. The byte order mark is
    the one named character with no block row: U+FEFF sits in Arabic
    Presentation Forms-B
    only because Unicode lays blocks out by contiguity
    and FE70..FEFF closes 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 needs UnicodeData.txt, which is a different order
    of thing from a table of ranges. See :help hexpair-inspect-naming.

  • :HexPairInspect works 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.cmd and vimhexdiff.cmd, the cmd.exe counterparts of the
    two shell functions in hexpair.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 on PATH and updating the plugin updates
    the commands; VIMHEX_VIM picks the Vim, gvim included.

  • Explorer context-menu entries, as a vimhex submenu 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 %2 without a COM handler - so
    vimhexdiff.cmd grew /left FILE and /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.cmd and gvimhexdiff.cmd, and gvimhex/gvimhexdiff in
    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
    for vim to run in or a way to pass VIMHEX_VIM. Each delegates to its
    vimhex/vimhexdiff counterpart rather than duplicating the argument
    grammar.

  • vimhex-contex-entry.add.reg and vimhex-contex-entry.remove.reg,
    ready-made versions of the registry snippets above - one import wires up
    all three context-menu entries (pointing at gvimhex.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\hexpair as REG_EXPAND_SZ
    values, the one registry string type whose %USERPROFILE% the shell
    expands - a plain REG_SZ would have it look for a folder literally
    named that. Installed elsewhere? make-context-entry-reg.py regenerates
    the pair for any path, which is also why these two files are generated
    rather than hand-written: .reg can only express that type as hex(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 .cmd file otherwise. A V mark in Vim's own green, a 0x
    badge marking these as hexpair's, and on the diff pair a bigger badge of
    two window panes (echoing vimhexdiff's own vsplit) - 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 by icons/build.py
    (icons/design.py, icons/rasticon.py - a from-scratch PNG/ICO
    encoder, no image library); only the .ico files ship in the release
    tarball, the generator is a development-only file.

  • :HexPairDiffShow (<Plug>(HexPairDiffShow), <Leader>D in
    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 be 00.

  • Files over 2 GiB on native Windows, which xxd cannot 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 nor xxd can
    shorten a file except by rewriting it, so a shrinking write copies the
    whole file on every other platform, while .NET's SetLength truncates
    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 = 0 opts out.

Changed

  • vimhexdiff opens maximized (:simalt ~x, guarded by
    has('gui_running')) and sets shortmess+=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.

  • :HexPairGoOffset takes $ for the file's last byte, the same
    shorthand :HexPairPageGoto already took for the last page - the two
    prompts sit under neighbouring keys (<Leader>b and <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 since count() over a string left the plugin — plus Vim
    9.0.0795 with +num64 for 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-file readblob() 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 with E118: Too many arguments for function: readblob part way through the copy. The gate, the
    message and the documented requirement all say 9.0.0795 now.
  • xxd was located only when a page was opened, so any other route
    into the readers died with E121: Undefined variable instead of a
    message naming xxd. It is resolved on demand now, and a missing xxd
    is reported as itself wherever it is first needed.
  • On native Windows, everything past 2 GiB was read and written at the
    wrong offset.
    xxd keeps its seek offset in a C longstrtol()
    into long seekoff, then fseek() — and a long is 32 bits on Windows,
    which is LLP64. Worse, strtol() saturates: an offset past the limit
    does not fail, it silently becomes 2147483647 and xxd reads, 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
    long is 64 bits. xxd -r and xxd -o share the limit, so the page you
    look at, the bytes a diff compares and the bytes a :w puts back were
    all affected. Everything past that limit now goes through PowerShell —
    see Windows and the 2 GiB limit in README.md.
  • A nonsensical g:hexpair_bytes_per_line was accepted. Zero passed
    the "page size must be a multiple of it" check, be...
Read more

Release v2.2.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 28 Aug 09:09
v2.2.0
f6a788b

[v2.2.0] – 2026-08-28

Added

  • :HexPairSyncViews brings every scroll-bound view onto the byte this
    one is on - the page holding it, the byte itself, and level.
    'scrollbind' promises that windows move together, not that they are
    on the same byte, and within a page they navigate independently on
    purpose; this is the way back from moving the cursor by hand. (A jump
    needs no help - see Fixed.) <Plug>(HexPairSyncViews), <Leader>= in
    hexpair.vimrc.
  • :HexPairInspect marks the bytes it just read (HexPairInspect,
    Visual by default), in both columns, for as long as the cursor stays
    on the byte they were read from. The report's first line already says
    which bytes it is about; the marking is what saves counting eight pairs
    of digits back off a line of forty-eight. Near the end of a page or of
    the file the report can only read three or four, and then three or four
    are marked - a marking of eight would be saying something the report
    does not. :HexPairInspect! takes it off at once,
    g:hexpair_show_inspect = 0 never draws it.
  • :HexPairInsertChar [++enc={encoding}] {text} puts the bytes of a
    character in at the cursor - the data inspector read backwards. It says
    what the bytes at the cursor would be as utf-8, utf-16 and utf-32; this
    writes a character in exactly those. Š is c5 a0, or 60 01, or
    00 00 01 60, and which of them is meant is
    g:hexpair_insert_encoding ('utf-8' unless you say otherwise) - a
    file is in one encoding, so the question is worth answering once, and
    ++enc= overrules it for a single insert. The bytes go in before the
    byte under the cursor and push the page along, exactly as typing them
    into the dump would, so one u takes the insert back and nothing
    reaches the file until :w does. utf-8, utf-16le/be, utf-32le/be,
    latin1 and ascii are computed from the code point rather than converted,
    because a Vim string cannot hold a NUL and A in utf-16le is 41 00;
    any other name is handed to iconv(), checked by converting it back,
    and refused if this Vim does not know it. <Plug>(HexPairInsertChar)
    asks for the text (<Leader>I in hexpair.vimrc).
  • docs/: the animation at the top of README.md, and what records
    it.
    The plugin at work on its own v2.1.0 release tarball, fetched
    live: a reproducible build, so the bytes on screen are the bytes anybody
    else gets, and the same 480 KiB can be fetched and followed along offset
    for offset. A binary opened as plain text and then as a
    hex page, the byte and its character lit up together as the cursor walks
    the columns, searches by text and by bytes across the whole file, a byte
    typed over and the ASCII column catching up on :HexPairRefresh, the two
    bytes of a multi-byte character read back by the data inspector, a
    character written in by its bytes, a write that says what a longer file
    costs, and vimhexdiff's two panes with their differences marked - shown
    through both the :HP* commands and the key mappings, because a recording
    made only of typed commands reads as if the plugin had no keys, and with
    every line left on the screen long enough to be read before it is sent.
    It is recorded rather than drawn
    (docs/hexpair-demo.sh), from this repository's own working tree, so
    it can be made again whenever what it shows stops being true.

Changed

  • <Leader>G is the suggested key for :HexPairPageGoto! - ask which
    page to go to, discarding unwritten changes - where hexpair.vimrc and
    the README.md example both said <Leader>P. It belongs beside
    <Leader>g, which is the same question without the bang, the way every
    other pair here is a letter and its capital; G was the go-to-mark key
    until v2.1.0 moved the marks under <Leader>m, and freeing it is what
    the pair was waiting for. The plugin still defines no key mappings of
    its own - this is the mapping file and the documentation.

Fixed

  • A jump takes the scroll-bound views along whether the page turned or
    not.
    It used to do it only when the byte was on another page, so the
    same keystroke moved the other window or left it behind depending on how
    far it happened to go. A jump names a byte and a byte means the same
    thing in every view of the file; moving the cursor by hand is the other
    thing, and stays independent.

  • vimhexdiff starts with both cursors on the first difference, which
    it has always said it does. 'scrollbind' syncs movement made from the
    moment a window was bound, and everything vimhexdiff does happens
    inside VimEnter - before the loop that would have done the syncing has
    run even once. The left window jumped to the first difference and the
    right one stayed at the top of page 1, showing a different part of a
    different file.

  • The question a length-changing write asks says which way the length
    changed.
    A page that grows by more than the tail behind it is cheaper
    to write the file afresh than to shift in place, and takes the same road
    as a shortening - where it announced itself as "Shortening a file means
    writing it afresh" about a file that was getting longer.

  • A jump to a byte on another page keeps the scroll-bound windows
    together.
    In vimhexdiff, walking the differences with
    :HexPairDiffNext / :HexPairDiffPrev came apart the moment a jump
    crossed a page boundary: the other window turned to the right page but
    stayed at its first byte, while this one went on to the byte it was
    going to - two windows scrolling in step through different parts of two
    files, which is the one thing 'scrollbind' is there to prevent. A
    page turn and the cursor's arrival are two steps, and the windows were
    being levelled between them: :syncbind, which is what tells Vim where
    level is after a page has been loaded under a window, also swallows the
    next scroll it would have followed - and that next scroll was this
    window going to the byte. The levelling now happens after both steps,
    and the bound window lands on the same byte rather than on the page's
    first. Everything that goes to a byte is fixed by the same change:
    :HexPairDiffNext / :HexPairDiffPrev, :HexPairFind and its
    repeats, :HexPairGoOffset, :HexPairGoMark and
    :HexPairModifiedNext / :HexPairModifiedPrev.

  • The progress line of a file-wide scan is readable, and CTRL-C stops
    the scan.
    Neither was true: the line was echoed and then wiped by the
    redraw meant to show it, dozens of times a second, so a search of a
    large file looked like a hang with a flicker in the message line; and
    every block read caught the interrupt along with everything else and
    went on to read the next block, so CTRL-C only worked if it landed in
    the sliver of time between two reads. It stops the scan now, wherever
    it is pressed, and answers hexpair: stopped - a scan only reads, so
    there is nothing to undo. The line also says how far it has got as a
    size, not only as a percentage, because one per cent of a 70 GiB file
    is 700 MB and minutes of reading, and a figure that does not move is a
    figure that says "hung":

    hexpair: searching 2.3 GiB of 70.0 GiB (3%, CTRL-C stops)
    
  • :HexPairDiff names the file it is comparing against the short way
    (~ for your home directory, relative where it is below the working
    directory), as the jump messages already did - and so does the one
    jump message that still spelled it out in full, "file is longer:
    its bytes from N on have nothing here to differ from", which of the
    three is the longest and so the likeliest to wrap. Spelled out in full
    the summary ran past the command line, wrapped, and cost a hit-enter
    prompt - which is not only a keystroke: the prompt holds the screen as
    it was, so the jump the next command made was not drawn until the
    prompt was dismissed, and the view looked like it had ignored it.

Release v2.1.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 24 Aug 09:41
v2.1.0
69a2dbd

[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.

Ch...

Read more

Release v2.0.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 21 Aug 18:17
v2.0.0
b735761

[v2.0.0] – 2026-08-21

Added

  • Paged large-file mode, the reason for the major version:
    :HexPairOpen {file} [page] shows one configurable-size page
    (g:hexpair_page_size, default 128 KiB) of an arbitrarily large file
    as a hex dump with absolute file offsets, without ever loading the
    rest of the file into a buffer — usable straight from the shell,
    e.g. vim -c 'HexPairOpen bigfile.bin'. :HexPairPageNext[!] /
    :HexPairPagePrev[!] / :HexPairPageGoto[!] {N} navigate between
    pages, refusing to discard unsaved changes without !;
    :HexPairPages reports the current page, total pages and byte
    range. Each page is bracketed by a decorative banner line (page
    number, byte range) highlighted via the new HexPairPageBanner
    group. HexPairOpenFile({file} [, {page}]) opens a page the same way as
    :HexPairOpen but as a direct function call, for scripts, mappings
    or shell wrappers that build the filename programmatically — safer
    than constructing an :HexPairOpen command-line string for a name
    containing a space or a literal $, which does not fully round-trip
    through the Ex command's own argument parsing. <Plug>(HexPairPageGoto)
    prompts for a page number instead of needing a typed :HexPairPageGoto {N}; <Plug>(HexPairPageGotoForce) is the same prompt but discards
    unsaved changes without asking, like the {N} variant with !.
  • :HexPairRefresh (<Plug>(HexPairRefresh)): regenerate the offset
    and ASCII columns from the current hex payload without writing to
    disk — the same round trip a toggle off followed by a toggle on
    would perform, but staying in hex mode. Validated like :w; an
    invalid dump refuses the refresh instead of being converted. The
    'modified' flag is unaffected — only the rendering changes, never
    a byte of content.
  • :w on a paged view writes just that page, by one of three
    mechanisms chosen by what the edit did to its length. An edit that
    KEPT the length - overwriting values, the common case - patches the
    page in place through xxd -r with the target as an argument: the
    file keeps its length and every byte outside the page keeps its
    content, at a cost independent of the file's size. An edit that
    INSERTED bytes moves only what follows them - the tail is shifted
    right in place with xxd and the page patched in, so the head of the
    file is never even read, appending to the last page moves nothing at
    all, and the temporary space needed is one block's worth of hex
    whatever the file's size; the tail is moved from the end backwards, so
    a byte is never overwritten before it has been copied and no second
    copy of it is kept. An edit that DELETED bytes writes the file afresh,
    because moving the tail left is the same operation but nothing in Vim
    or xxd can then shorten the file: head, edited page and tail are
    block-copied (8 MiB blocks, so memory does not follow the file's size)
    into a temporary file, which replaces the original by being copied
    back over it, keeping its inode, owner and permissions - and if that
    copy back fails part way through, the temporary file holds the
    complete new content and its path is reported rather than deleted.
    Either change of length says by how much the file will change and how
    many bytes that writes, and asks first; g:hexpair_page_confirm = 0
    answers yes automatically, for scripts. Before any of them the dump is
    validated exactly as in the whole-file mode, and the file's size and
    modification time are compared with what they were when the page was
    read - a file that changed on disk meanwhile is refused rather than
    patched at offsets that may no longer mean anything. Afterwards the
    page is re-read from disk and the cursor returns to the byte it was
    on, even when a splice moved every byte behind it; a shrinking write
    that empties the file leaves a view saying so instead of a stale dump.
  • The vimhex shell wrapper now ships as hexpair.bashrc in the plugin
    directory, so it can be sourced from ~/.bashrc rather than copied
    out of the documentation:
    source ~/.vim/pack/plugins/start/hexpair/hexpair.bashrc. It handles
    - for standard input, a page number or an @BYTE position, and
    $VIMHEX_VIM picks a particular Vim.
  • :HexPairPages also reports the byte under the cursor, in hex with
    the decimal in brackets and 1-based — exactly the form
    :HexPairGoOffset and the vimhex wrapper's @BYTE take, so a
    position can be written down and gone back to.
  • A Visual selection is mirrored in the other column, the way the
    byte under the cursor already was: select hex digits and the text they
    are is highlighted, select text and the bytes it is are highlighted.
    Characterwise, linewise and blockwise selections all work, and one
    spanning several lines is mirrored line by line. Only the part on
    screen is mirrored, which keeps the work per cursor movement the same
    however much of a page is selected.
  • :HexPairGoOffset[!] {byte} jumps straight to a byte, decimal or
    0x-prefixed, turning the page it falls on and leaving you in
    whichever view you were in. The position is 1-based — byte 1 is the
    file's first byte, the numbering the page banner and :HexPairPages
    already use, so a number read off the banner can be typed back in. Pages are fixed-size slices, so the page
    holding an offset is a division. <Plug>(HexPairGoOffset) prompts for
    the offset the way <Plug>(HexPairPageGoto) prompts for a page;
    <Plug>(HexPairGoOffsetForce) is the ! variant.
  • :w {file} on a paged view writes the entire content being paged,
    with the current page's edits in it, to {file}, leaving the original
    alone — a save-as rather than a refusal. For a view paged from piped
    input (cat x | vim -) that is the only way to save at all, and the
    view adopts {file} afterwards, so a later plain :w patches pages
    into it. hexpair also warns when it pages a buffer that was not read
    in binary mode, since piped input cannot be re-read with ++bin the
    way a named file can.
  • <Plug>(HexPairPages), so every command that takes no argument now
    has a <Plug> target and can be bound to a key — the README and
    :help hexpair-mappings list the whole set on a <Leader> prefix.

Changed

  • One hex mode, always paged. :HexPairToggle no longer converts a
    whole buffer: it shows one page, always with the banner — a small
    file simply has exactly one — and toggles from there to a windowed
    text view
    of the same page's raw bytes and back. There is
    deliberately no way back to the plain buffer, since a buffer holding
    one page is not the file and a plain :w would truncate the file
    down to it; every hex-mode buffer is buftype=acwrite with the
    page-range write path for the same reason. Where a toggled buffer's
    pages come from depends on what it was: an unmodified file-backed
    buffer is paged from its file, an unnamed one (cat x | vim -) from
    a private temp it is spilled into, and a modified file-backed one is
    refused — the buffer and the file disagree, and both ways of
    resolving that lose edits quietly.
  • A file with no bytes is viewable - it simply has no pages and the view
    says so - and :e re-reads the current page.
  • Pages are plain fixed-size slices: the paged view reads the width of
    each line's offset column off the line itself, so a page may
    span the point at 4 GiB where xxd widens that column from eight hex
    digits to nine, instead of page boundaries being clamped to keep each
    page uniform. Page N always starts at (N-1) * g:hexpair_page_size,
    page numbering no longer shifts when a file grows past 4 GiB, and a
    bare hex line with no offset column at all is laid out correctly too.
  • Only a write that shortens a file - and :w {file}, and a growing
    write whose tail is more than half the file, both of which go through
    the same splice - requires Vim patch 8.2.4906 with +num64, for
    readblob(), checked when such a write is attempted and refusing just
    that write. Viewing pages, navigating them, same-length writes and
    inserts work on the Vim 8.0 baseline the rest of the plugin requires.
  • Writing a page walks it once instead of three times (validation,
    cursor mapping and stripping share one scan): a write on a 128 KiB
    page went from 0.5 s to 0.32 s, and the saving scales with the page
    size.

Fixed

  • An empty file grew to one byte when it went through the hex view.
    Vim serializes an empty buffer for a filter as a single newline, so
    the dump showed a 0a the file did not contain and writing it back
    created one. Deleting the whole dump and writing now also produces an
    empty file rather than a one-byte one. A file that really holds a
    lone 0a looks identical in the buffer and still dumps that byte.
  • Data loss: toggling hex mode off used to unconditionally mirror the
    buffer's modified state from BEFORE hex mode was entered, so an edit
    made to the dump on an until-then-unmodified buffer silently cleared
    'modified' on toggle-off — :q would then discard it without a
    warning. The buffer is now tracked for real content changes made
    while in hex mode (via b:changedtick, unaffected by cursor
    movement) independently of the pre-hex-mode state, in both
    directions: a pre-existing unsaved change is still preserved, and an
    edit made purely in hex mode now correctly marks the buffer modified
    on toggle-off.

Release v1.1.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 19 Jul 17:55
v1.1.0
7e2c765

[v1.1.0] – 2026-07-19

Added

  • Bundled filetype plugin (ftplugin/xxd.vim) with dump-editing
    defaults — tabstop=10, expandtab, shiftwidth=3 (one hex byte),
    no automatic formatting, wrapping or indenting — applied to any
    buffer with filetype=xxd and fully reverted via b:undo_ftplugin
    when the hex view is toggled off (including buffers whose original
    filetype was empty, where no FileType event fires). Suppress with
    let b:did_ftplugin = 1 in a personal ftplugin/xxd.vim, or
    override individual settings in after/ftplugin/xxd.vim
    (:help hexpair-ftplugin).

  • g:hexpair_paste (default on): the global 'paste' option is
    switched on while the cursor is in a hex-mode buffer and restored to
    its previous value when the cursor leaves it or hex mode is toggled
    off, so insert-mode mappings and abbreviations cannot mangle typed
    hex; all other buffers keep the user's own 'paste' state. The
    buffer's 'expandtab' is preserved across the switch.

  • Dump validation: a character in the hex area that is not a hex
    digit, or an odd total number of hex digits, now aborts :w and
    hex-mode toggle-off with an error and the cursor parked on the
    offender, instead of silently dropping data — the file on disk and
    the dump keep their previous content. This also limits the damage
    after an u that undid the conversion itself: the non-dump content
    is refused rather than converted.

  • :e / :e! while hex mode is active now regenerates the dump from
    the freshly read file and keeps hex mode and the cursor byte offset,
    instead of leaving raw binary content in a buffer that still
    believed it was a dump.

Changed

  • Documented mapping examples now use the conventional <Leader>
    prefix instead of §, which only exists on some keyboard layouts
    (e.g. Czech); the plugin still defines no mappings of its own. The
    help and README now also explain the <Leader>/mapleader and
    <Plug> mechanisms for readers new to them.

Release v1.0.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 19 Jul 13:57
v1.0.0
1840a95

hexpair Vim plugin turns the classic :%!xxd workflow into a small, reliable
hex editor for Vim — pure VimScript plus the xxd utility that ships
with Vim, behaving identically on Linux, native Windows (Vim/gVim)
and WSL.

Highlights

  • Live pair highlighting. The byte under the cursor is highlighted
    in both columns of the dump: stand on a hex pair and the matching
    ASCII character lights up, and vice versa. Direction-aware groups
    (HexPairActive for the cursor side, HexPairMirror for the
    counterpart) show at a glance which column you are in.
  • Byte-exact cursor mapping. Toggling between the normal view and
    the hex view keeps the cursor on the very same byte — exact even for
    files with a BOM, CRLF or mixed CRLF/LF line endings, and
    single-byte file encodings (utf-16 remains approximate).
  • Safe :w in hex mode. The buffer is transparently converted
    back to binary before the write and the dump is regenerated
    afterwards — the file on disk always receives the real bytes, never
    the textual dump, and the cursor stays on its byte even when
    insertions shifted all offsets.
  • Forgiving editing. Only the HEX column matters: the offset and
    ASCII columns are purely decorative and are stripped before the
    reverse conversion. Insert, delete or reorder lines freely —
    inserted lines need no offset and no ASCII part; offsets and the
    ASCII column self-heal on the next :w.
  • Column navigation. :HexPairGoHex, :HexPairGoAscii and
    :HexPairSwap jump between the two representations of the byte
    under the cursor.
  • Binary correctness. A file opened without vim -b is reloaded
    with :edit ++bin on the first toggle, so the dump always shows the
    exact on-disk bytes and a later :w cannot silently re-encode the
    file.
  • No default key mappings. Commands and <Plug> mappings only —
    you choose your own keys.

Installation

mkdir -p ~/.vim/pack/plugins/start
tar xf hexpair.v1.0.0.tar -C ~/.vim/pack/plugins/start/
vim -c 'helptags ALL' -c 'q'

Then add your own mappings to ~/.vimrc, for example:

nmap §h <Plug>(HexPairToggle)     " toggle hex view
nmap §< <Plug>(HexPairGoHex)      " jump to the HEX column (same byte)
nmap §> <Plug>(HexPairGoAscii)    " jump to the ASCII column (same byte)
nmap §- <Plug>(HexPairSwap)       " jump to the opposite column

Full documentation: :help hexpair after installation.

Requirements

  • Vim 8.0+ (native packages; the plugin uses lambda expressions).
  • The xxd utility, which ships with Vim — looked up on PATH first
    and then in $VIMRUNTIME, so on Windows the bundled xxd.exe is
    found even when it is not on PATH.

Verifying the download

The release tarball is reproducible: packaging the sources of this
tag with ./pack-release (Linux, MacOS, ...) or .\pack-release.cmd (Windows)
produces a byte-identical archive on any platform, so its SHA-256 can be
independently re-derived. The tarball is signed with GPG key
489C 5EC8 0FD6 2BE8 9E59 B4F7 19C1 3E8C E0F5 DB61:

gpg --keyserver keys.openpgp.org --recv-keys 489C5EC80FD62BE89E59B4F719C13E8CE0F5DB61
gpg --verify hexpair.v1.0.0.tar.asc hexpair.v1.0.0.tar

About this release

v1.0.0 is the first public release, consolidating fourteen internal
iterations that shaped the plugin before this import — the evolution
notes are in CHANGELOG.md.