1

Huh?

From this interesting post, I took the idea to incorporate xxd into vim a little more nicely than the usual %!xxd.

So I started structuring the code and testing. (It's a first draft.)

Link to code (also listed below)

The issue

The problem I ran into is that, after doing :Hex, the 'modified' option is always set.

As best as I can tell, it's because the conversion to hex triggers the TextChanged autocmd-event, which then modifies the text. When the ftplugin script actually finishes, 'modified' is still unset (as noticed using :debug).

In a similar vein, anytime I save (:w), the modified flag stays on. This one I'm less sure about, but I'd bet it's a similar issue.

Attempted fix

Removing the TextChanged portion of the autocommand basically solves all my problems.

Unfortunately, I liked its other effects. When using <C-x> and <C-a>, or even r, the text corresponding the to the hex was instantly updated. I could probably achieve this using CursorHold or something else, but I do want it to be rather instantaneous.

(I've already checked, and noautocmd won't help me here; TextChanged explicitly ignores it.)

Code

" ~/.vim/plugin/hex.vim
if exists('g:loaded_hex')
  finish
endif
let g:loaded_hex = 1

command -bar -bang Hex
      \ if <bang>0 |
      \   set filetype= |
      \   edit! |
      \ else |
      \   set filetype=xxd |
      \ endif
command -bar Bless Hex!
" ~/.vim/after/ftplugin/xxd.vim
" Vim filetype plugin
" Language:              xxd
" Maintainer:            Ben Knoble <ben.knoble@gmail.com>

" cache it
let g:xxd = get(g:, 'xxd', executable('xxd'))

if !g:xxd
  finish
endif

call hex#setup()
" ~/.vim/autoload/hex.vim
" I realized this should be probably all just be scriptlocal in the ftplugin,
" but oh well. It's not hurting anyone
function hex#setup() abort
  setlocal binary
  call hex#dump()
  setlocal nomodified

  augroup ftplugin_xxd
    au! * <buffer>

    au BufWritePre <buffer> call hex#write_pre()
    au BufWritePost <buffer> call hex#write_post()
    au TextChanged,InsertLeave <buffer> call hex#update()
  augroup END

  " this comes from another of my autoload files. You can check it in the github
  " link above if you want
  let b:undo_ftplugin = ftplugin#undo({
        \ 'opts': [
        \   'binary',
        \ ],
        \ 'custom': [
        \   'execute "au! ftplugin_xxd * <buffer>"',
        \   'call hex#rev()',
        \ ],
        \ })
endfunction

function hex#dump() abort
  silent %!xxd -g 1
  call hex#strip_trailing_CR()
endfunction

function hex#rev() abort
  silent %!xxd -r
endfunction

function hex#strip_trailing_CR() abort
  %substitute/\r$//e
endfunction

" before writing, translate back to original
function hex#write_pre() abort
  call hex#save_cursor()
  call hex#rev()
endfunction

" after writing, restore hex view and mark unmodified
function hex#write_post() abort
  call hex#dump()
  setlocal nomodified
  call hex#restore_cursor()
endfunction

" update text column after changing hex values
function hex#update() abort
  call hex#save_cursor()
  call hex#rev()
  call hex#dump()
  call hex#restore_cursor()
endfunction

function hex#save_cursor() abort
  let b:xxd_cursor = getpos('.')
endfunction

function hex#restore_cursor() abort
  call setpos('.', b:xxd_cursor)
endfunction
  • 1
    What about just adding setlocal nomodified here and there? – Matt Oct 24 '19 at 4:59
  • 1
    I am not sure I understand. Can you post a minimal example of how the modified flag is set unwantedly? – Christian Brabandt Oct 24 '19 at 6:37
  • 1
    @ChristianBrabandt essentially, if you run Hex, the modified flag is set. After you save, it’s still set. Despite not editing anything. Because the conversions back and forth trigger TextChanged (I think). – D. Ben Knoble Oct 24 '19 at 10:44
1
                            *TextChanged*
TextChanged         After a change was made to the text in the
                current buffer in Normal mode.  That is after
                |b:changedtick| has changed (also when that
                happened before the TextChanged autocommand
                was defined).

It's based on :h b:changedtick , so change :h 'modified' won't stop it.

Although your TextChanged is installed in the end of you hex#setup(), the change of b:changedtick happened before (hex#dump()) still count. The help only says before the TextChanged autocommand was defined, but it doesn't say it's starting point, i guess in this case it's the Hex command, correct me if I'm wrong.

Your plugin have redefined the meaning of change:

It's a change if it's hex content is changed.

You can use another changedtick to rule out other changes.

function! s:update_hex_changedtick() abort
  let b:hex_changedtick = b:changedtick
endfunction

function hex#setup() abort

  setlocal binary
  call hex#dump()
  call s:update_hex_changedtick()
  ...
endfunction

function hex#write_post() abort
  ...
  call s:update_hex_changedtick()
endfunction

Use it to decide whether hex changed happens:

function! hex#changed() abort
  return b:changedtick > get(b:, 'hex_changedtick', 0)
endfunction

function hex#update() abort
  " get changed before b:changetick changed.
  let changed = hex#changed()
  ...
  if !changed
    set nomodified
  endif
endfunction

This should fix all your problems. My test code doesn't include the ftplugin#undo part.

| improve this answer | |
  • Very interesting. Im not sure I completely understand how it works, but I’ll give it a look. – D. Ben Knoble Oct 24 '19 at 10:44
  • Welp, it works! Accepting. Thanks for that. (Now to make it play well with undo 🤔) – D. Ben Knoble Oct 25 '19 at 14:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.