" flydiff - on-the-fly diff
" Version: 0.0.1
" Copyright (C) 2008 kana
" License: MIT license (see )
" Constants "{{{1
let s:FALSE = 0
let s:TRUE = !s:FALSE
let s:ON = 1
let s:OFF = 0
let s:TOGGLE = -773
let s:INVALID_BUFNR = -1 " same as bufnr('no such buffer')
let s:INVALID_WINNR = -1 " same as bufwinnr('no such buffer')
let s:INVALID_CHANGEDTICK = -3846
let s:TYPE_DIFF_BUFFER = ['diff buffer']
let s:TYPE_NORMAL_BUFFER = ['normal buffer']
" Interface "{{{1
function! flydiff#toggle(bufnr, state) "{{{2
if !bufexists(a:bufnr)
echoerr 'No such buffer:' a:bufnr
return s:FALSE
endif
let b_flydiff_info = s:flydiff_info(a:bufnr, s:TYPE_NORMAL_BUFFER)
if b_flydiff_info.type isnot s:TYPE_NORMAL_BUFFER
echoerr 'Flydiff is only performed for normal buffers:' a:bufnr
return s:TRUE
endif
let state = a:state ==? 'on' ? s:ON : (a:state ==? 'off' ? s:OFF : s:TOGGLE)
if state == s:TOGGLE
let state = b_flydiff_info.state == s:OFF ? s:ON : s:OFF
endif
if b_flydiff_info.state == state
return s:TRUE " nothing to do -- on-the-fly diff is already on or off.
endif
if state == s:ON
call s:set_flydiff_handlers()
else " state == s:OFF
call s:remove_flydiff_handlers()
endif
let b_flydiff_info.state = state
return s:TRUE
endfunction
" Misc. "{{{1
function! s:create_diff_buffer_for(bufnr) "{{{2
" Create diff buffer for a:bufnr and set up misc. options.
let original_bufnr = bufnr('')
let original_bufhidden = &l:bufhidden
let &l:bufhidden = 'hide'
hide enew
setlocal bufhidden=hide nobuflisted buftype=nofile nomodifiable noswapfile
execute 'setfiletype' g:flydiff_filetype
silent file `=printf('*flydiff* (%d) %s',
\ original_bufnr, bufname(original_bufnr))`
" Set base_bufnr.
let diff_bufnr = bufnr('')
let b_flydiff_info = s:flydiff_info(diff_bufnr, s:TYPE_DIFF_BUFFER)
let b_flydiff_info.base_bufnr = a:bufnr
" Restore to the original buffer original_bufnr.
" Note that original_bufnr may not be equal to a:bufnr.
silent execute original_bufnr 'buffer'
let &l:bufhidden = original_bufhidden
return diff_bufnr
endfunction
function! s:empty_buffer_p(bufnr) "{{{2
return line('$') == 1 && getline(1) == ''
endfunction
function! s:flydiff_info(bufnr, ...) "{{{2
let bufvars = getbufvar(a:bufnr, '')
if !has_key(bufvars, 'flydiff_info')
if a:0 == 0
throw 'Internal error: buffer ' . a:bufnr . ' is not related to flydiff'
endif
let bufvars.flydiff_info = {'type': a:1}
if a:1 is s:TYPE_NORMAL_BUFFER
let bufvars.flydiff_info.state = s:OFF
let bufvars.flydiff_info.diff_bufnr = s:INVALID_BUFNR
elseif a:1 is s:TYPE_DIFF_BUFFER
let bufvars.flydiff_info.base_bufnr = s:INVALID_BUFNR
let bufvars.flydiff_info.not_performed_p = s:TRUE
else
throw printf('Internal error: Invalid type %s for %d',
\ string(a:1), a:bufnr)
endif
endif
return bufvars.flydiff_info
endfunction
function! s:flydiff_timing() "{{{2
" MEMO: to customize flydiff_timing for each buffer:
" return exists('b:flydiff_timing') ? b:flydiff_timing : g:flydiff_timing
return g:flydiff_timing
endfunction
function! s:flydiff_direction() "{{{2
return g:flydiff_direction
endfunction
function! s:open_diff_buffer_window(bufnr) "{{{2
let diff_winnr = bufwinnr(a:bufnr)
if diff_winnr != s:INVALID_WINNR
return diff_winnr
endif
let v:errmsg = ''
execute s:flydiff_direction() 'split'
if v:errmsg != ''
return s:INVALID_WINNR
endif
silent execute a:bufnr 'buffer'
let diff_winnr = winnr()
wincmd p
return diff_winnr
endfunction
function! s:perform_flydiff(timing) "{{{2
" Use , because the current buffer may not be same as .
let base_bufnr = str2nr(expand('')) " bufnr must be a number.
let b_flydiff_info = s:flydiff_info(base_bufnr)
if !bufexists(b_flydiff_info.diff_bufnr)
let b_flydiff_info.diff_bufnr = s:create_diff_buffer_for(base_bufnr)
if !bufexists(b_flydiff_info.diff_bufnr)
echoerr 'Unable to create a diff buffer for' base_bufnr
return s:FALSE
endif
endif
let diff_winnr = s:open_diff_buffer_window(b_flydiff_info.diff_bufnr)
if diff_winnr == s:INVALID_WINNR
echoerr 'Unable to open a window for diff buffer for' base_bufnr
return s:FALSE
endif
" There is another method which checks b:changedtick to determine whether
" the buffer is modified or not. This method is more accurate than the
" method which checks &l:modified, because &l:modified may be unset when
" user does work quickly in 'updatetime'.
"
" But b:changedtick is a special variable and it cannot be accessed via
" getbufvar(), so another method cannot be used for the case of this plugin.
let diff_b_flydiff_info = s:flydiff_info(b_flydiff_info.diff_bufnr)
if diff_b_flydiff_info.not_performed_p
\ || a:timing ==# 'written'
\ || getbufvar(base_bufnr, '&modified')
let diff_b_flydiff_info.not_performed_p = s:FALSE
silent update
let base_buffer_linenr = line('.')
execute diff_winnr 'wincmd w'
setlocal modifiable
silent % delete _ " suppress '--No lines in buffer--' message.
silent execute 'read !' s:vcs_diff_script(base_bufnr)
1 delete _
call s:_adjust_cursor(base_buffer_linenr)
if s:empty_buffer_p(b_flydiff_info.diff_bufnr) " == bufnr('')
call setline(1, '=== No difference ===')
endif
setlocal nomodifiable
wincmd p
endif
return s:TRUE
endfunction
function! s:_adjust_cursor(base_line) "{{{3
" Adjust the cursor in a diff buffer to point the line which is equivalent
" to a:base_line in the corresponding base buffer. This adjustment is
" useful when there are many differences and they cannot be showed in the
" diff buffer window at once.
"
" Assumptions:
" - The current buffer is a diff buffer.
" - The content of the diff buffer is diff --unified.
let diff_header_lines = []
silent global/^@@ .* @@/call add(diff_header_lines, line('.'))
let following_diff_block_head = 0
for diff_header_line in diff_header_lines
let _ = substitute(getline(diff_header_line),
\ '^@@ -\d\+,\d\+ +\(\d\+\),\(\d\+\) @@.*$',
\ '\1,\2',
\ '')
let [d_line1, d_lineN] = split(_, ',')
let d_line2 = d_line1 + d_lineN - 1
if a:base_line < d_line1 " this is a following diff block.
let following_diff_block_head = diff_header_line
break " rest of diff blocks never includes a:base_line.
elseif d_line2 < a:base_line " this is a preceding diff block.
" ignore
else " if d_line1 <= a:base_line && a:base_line <= d_line2
let delta = 0
for i in range(1 + a:base_line - d_line1)
let delta += 1
" skip lines with "deleted" marker "-".
while getline(diff_header_line + delta)[0] == '-'
let delta += 1
endwhile
endfor
call cursor(diff_header_line + delta, 0)
normal! zz
return
endif
endfor
" If there is no diff block which contains a:base_line.
call cursor((following_diff_block_head
\ ? following_diff_block_head
\ : line('$')),
\ 0)
return
endfunction
function! s:remove_flydiff_handlers() "{{{2
augroup plugin-flydiff
autocmd! *
augroup END
return
endfunction
function! s:set_flydiff_handlers() "{{{2
augroup plugin-flydiff
autocmd BufWritePost
\ if s:flydiff_timing() =~# '\'
\ | call s:perform_flydiff('written')
\ | endif
autocmd CursorHold
\ if s:flydiff_timing() =~# '\'
\ | call s:perform_flydiff('realtime')
\ | endif
autocmd CursorHoldI
\ if s:flydiff_timing() =~# '\'
\ | call s:perform_flydiff('realtime')
\ | endif
augroup END
return
endfunction
function! s:vcs_diff_script(bufnr) "{{{2
" Return a string of the shell script to get difference between the
" currently edited a:bufnr and the latest version of a:bufnr with
" appropriate version control system.
"
" FIXME: Support version control systems other than git.
" FIXME: Support shells other than ordinary ones for *nix.
let full_path = fnamemodify(bufname(a:bufnr), ':p')
let working_directory = fnamemodify(full_path, ':h')
return printf('cd %s; git-diff -- %s',
\ fnameescape(working_directory),
\ fnameescape(full_path))
endfunction
" __END__ "{{{1
" vim: foldmethod=marker