Skip to content

Release v2.1.0

Choose a tag to compare

@michal-ruzicka michal-ruzicka released this 24 Aug 09:41
· 65 commits to main since this release
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.

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.