changeset 61:ea794e48d4e2

Better way to handle buffer actions: - Added buffer object with on delete/windows leave callbacks. - Added helper functions to open and delete "dependency" buffers. - Using that new system to handle diffs opened by `Hgannotate` and `Hglog`.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 23 Nov 2012 13:43:08 -0800
parents 137d5c895659
children 136be8fa8710
files plugin/lawrencium.vim
diffstat 1 files changed, 220 insertions(+), 61 deletions(-) [+]
line wrap: on
line diff
--- a/plugin/lawrencium.vim	Wed Nov 14 22:31:33 2012 -0800
+++ b/plugin/lawrencium.vim	Fri Nov 23 13:43:08 2012 -0800
@@ -32,6 +32,10 @@
     let g:lawrencium_define_mappings = 1
 endif
 
+if !exists('g:lawrencium_auto_close_buffers')
+    let g:lawrencium_auto_close_buffers = 1
+endif
+
 if !exists('g:lawrencium_annotate_width_offset')
     let g:lawrencium_annotate_width_offset = 0
 endif
@@ -160,6 +164,73 @@
     return l:result
 endfunction
 
+" Finds a window whose displayed buffer has a given variable
+" set to the given value.
+function! s:find_buffer_window(varname, varvalue) abort
+    for wnr in range(1, winnr('$'))
+        let l:bnr = winbufnr(wnr)
+        if getbufvar(l:bnr, a:varname) == a:varvalue
+            return l:wnr
+        endif
+    endfor
+    return -1
+endfunction
+
+" Opens a buffer in a way that makes it easy to delete it later:
+" - if the about-to-be previous buffer doesn't have a given variable,
+"   just open the new buffer.
+" - if the about-to-be previous buffer has a given variable, open the
+"   new buffer with the `keepalt` option to make it so that the
+"   actual previous buffer (returned by things like `bufname('#')`)
+"   is the original buffer that was there before the first deletable
+"   buffer was opened.
+function! s:edit_deletable_buffer(varname, varvalue, path) abort
+    let l:edit_cmd = 'edit '
+    if getbufvar('%', a:varname) != ''
+        let l:edit_cmd = 'keepalt edit '
+    endif
+    execute l:edit_cmd . fnameescape(a:path)
+    call setbufvar('%', a:varname, a:varvalue)
+endfunction
+
+function! s:delete_dependency_buffers(varname, varvalue) abort
+    let l:cur_winnr = winnr()
+    for bnr in range(1, bufnr('$'))
+        if getbufvar(bnr, a:varname) == a:varvalue
+            " Delete this buffer if it is not shown in any window.
+            " Otherwise, display the alternate buffer before deleting
+            " it so the window is not closed.
+            let l:bwnr = bufwinnr(bnr)
+            if l:bwnr < 0 || getbufvar(bnr, 'lawrencium_quit_on_delete') == 1
+                if bufloaded(l:bnr)
+                    call s:trace("Deleting dependency buffer " . bnr)
+                    execute "bdelete! " . bnr
+                else
+                    call s:trace("Dependency buffer " . bnr . " is already unladed.")
+                endif
+            else
+                execute l:bwnr . "wincmd w"
+                " TODO: better handle case where there's no previous/alternate buffer?
+                if bufnr('#') > 0 && bufloaded(bufnr('#'))
+                    bprevious
+                    if bufloaded(l:bnr)
+                        call s:trace("Deleting dependency buffer " . bnr . " after buffer switching.")
+                        execute "bdelete! " . bnr
+                    else
+                        call s:trace("Dependency buffer " . bnr . " is unladed after buffer switching.")
+                    endif
+                else
+                    call s:trace("Deleting dependency buffer " . bnr . " and window.")
+                    bdelete!
+                endif
+            endif
+        endif
+    endfor
+    if l:cur_winnr != winnr()
+        execute l:cur_winnr . "wincmd w"
+    endif
+endfunction
+
 " }}}
 
 " Mercurial Repository {{{
@@ -321,6 +392,96 @@
 
 " }}}
 
+" Buffer Object {{{
+
+" The prototype dictionary.
+let s:Buffer = {}
+
+" Constructor.
+function! s:Buffer.New(number) dict abort
+    let l:newBuffer = copy(self)
+    let l:newBuffer.nr = a:number
+    let l:newBuffer.var_backup = {}
+    let l:newBuffer.on_delete = []
+    let l:newBuffer.on_winleave = []
+    execute 'augroup lawrencium_buffer_' . a:number
+    execute '  autocmd!'
+    execute '  autocmd BufDelete <buffer=' . a:number . '> call s:buffer_on_delete(' . a:number . ')'
+    execute 'augroup end'
+    call s:trace("Built new buffer object for buffer: " . a:number)
+    return l:newBuffer
+endfunction
+
+function! s:Buffer.GetVar(var) dict abort
+    return getbufvar(self.nr, a:var)
+endfunction
+
+function! s:Buffer.SetVar(var, value) dict abort
+    if !has_key(self.var_backup, a:var)
+        self.var_backup[a:var] = getbufvar(self.nr, a:var)
+    endif
+    return setbufvar(self.nr, a:var, a:value)
+endfunction
+
+function! s:Buffer.RestoreVars() dict abort
+    for key in keys(self.var_backup)
+        setbufvar(self.nr, key, self.var_backup[key])
+    endfor
+endfunction
+
+function! s:Buffer.OnDelete(cmd) dict abort
+    call s:trace("Adding BufDelete callback for buffer " . self.nr . ": " . a:cmd)
+    call add(self.on_delete, a:cmd)
+endfunction
+
+function! s:Buffer.OnWinLeave(cmd) dict abort
+    if len(self.on_winleave) == 0
+        call s:trace("Adding BufWinLeave auto-command on buffer " . self.nr)
+        execute 'autocmd BufWinLeave <buffer=' . self.nr . '> call s:buffer_on_winleave(' . self.nr .')'
+    endif
+    call s:trace("Adding BufWinLeave callback for buffer " . self.nr . ": " . a:cmd)
+    call add(self.on_winleave, a:cmd)
+endfunction
+
+let s:buffer_objects = {}
+
+" Get a buffer instance for the specified buffer number, or the
+" current buffer if nothing is specified.
+function! s:buffer_obj(...) abort
+    let l:bufnr = a:0 ? a:1 : bufnr('%')
+    if !has_key(s:buffer_objects, l:bufnr)
+        let s:buffer_objects[l:bufnr] = s:Buffer.New(l:bufnr)
+    endif
+    return s:buffer_objects[l:bufnr]
+endfunction
+
+" Execute all the "on delete" callbacks.
+function! s:buffer_on_delete(number) abort
+    let l:bufobj = s:buffer_objects[a:number]
+    call s:trace("Calling BufDelete callbacks on buffer " . l:bufobj.nr)
+    for cmd in l:bufobj.on_delete
+        call s:trace(" [" . cmd . "]")
+        execute cmd
+    endfor
+    call s:trace("Deleted buffer object " . l:bufobj.nr)
+    call remove(s:buffer_objects, l:bufobj.nr)
+    execute 'augroup lawrencium_buffer_' . l:bufobj.nr
+    execute '  autocmd!'
+    execute 'augroup end'
+endfunction
+
+" Execute all the "on winleave" callbacks.
+function! s:buffer_on_winleave(number) abort
+    let l:bufobj = s:buffer_objects[a:number]
+    call s:trace("Calling BufWinLeave callbacks on buffer " . l:bufobj.nr)
+    for cmd in l:bufobj.on_winleave
+        call s:trace(" [" . cmd . "]")
+        execute cmd
+    endfor
+endfunction
+
+" }}}
+
 " Buffer Commands Management {{{
 
 " Store the commands for Lawrencium-enabled buffers so that we can add them in
@@ -508,7 +669,7 @@
     command! -buffer          Hgstatusdiff      :call s:HgStatus_Diff(0)
     command! -buffer          Hgstatusvdiff     :call s:HgStatus_Diff(1)
     command! -buffer          Hgstatusdiffsum   :call s:HgStatus_DiffSummary(0)
-    command! -buffer          Hgstatusvdiffsum   :call s:HgStatus_DiffSummary(1)
+    command! -buffer          Hgstatusvdiffsum  :call s:HgStatus_DiffSummary(1)
     command! -buffer          Hgstatusrefresh   :call s:HgStatus_Refresh()
     command! -buffer -range   Hgstatusaddremove :call s:HgStatus_AddRemove(<line1>, <line2>)
     command! -buffer -range=% -bang Hgstatuscommit  :call s:HgStatus_Commit(<line1>, <line2>, <bang>0, 0)
@@ -1070,53 +1231,15 @@
     endif
 
     " Clean up when the log buffer is deleted.
-    execute 'autocmd BufDelete <buffer> call s:HgLog_Delete(' . a:is_file . ', "' . fnameescape(l:temp_file) . '")'
+    let l:bufobj = s:buffer_obj()
+    call l:bufobj.OnDelete('call s:HgLog_Delete(' . l:bufobj.nr . ', "' . fnameescape(l:temp_file) . '")')
 endfunction
 
-function! s:HgLog_Delete(was_file, path)
-    let l:repo = s:hg_repo()
-    let l:orignr = winnr()
-    let l:origedit = b:lawrencium_original_path
-    let l:origroot = s:stripslash(b:mercurial_dir)
-    let l:origpath = s:stripslash(b:lawrencium_logged_path)
-    call s:trace("Cleaning up '" . a:path . "', opened from '" . l:origedit . "'")
-    " Delete any other buffer opened by this log.
-    " (buffers with Lawrencium paths that match this repo and filename)
-    for nr in range(1, winnr('$'))
-        let l:br = winbufnr(nr)
-        let l:bpath = bufname(l:br)
-        let l:bpath_comps = s:parse_lawrencium_path(l:bpath)
-        if l:bpath_comps['root'] != ''
-            let l:bpath_root = s:normalizepath(l:bpath_comps['root'])
-            let l:bpath_path = s:normalizepath(s:stripslash(l:bpath_comps['path']))
-            call s:trace("Comparing '".l:bpath_path."' and '".l:origpath."' for cleanup.")
-            if l:bpath_root == l:origroot && l:bpath_path == l:origpath
-                " Go to that window and switch to the previous buffer
-                " from the buffer with the file revision.
-                " Just switching away should delete the buffer since it
-                " has `bufhidden=delete`.
-                execute nr . 'wincmd w'
-                let l:altbufname = s:shellslash(bufname('#'))
-                if l:altbufname =~# '\v^lawrencium://'
-                    " This is a special Lawrencium buffer... it could be
-                    " a previously shown revision of the file opened with
-                    " this very `Hglog`, which we don't want to switch to.
-                    " Let's just default to editing the original file
-                    " again... not sure what else to do here...
-                    call s:trace("Reverting to editing: " . l:origedit)
-                    execute 'edit ' . l:origedit
-                else
-                    bprevious
-                endif
-            endif
-        endif
-    endfor
-    " Restore the current window if we switched away.
-    let l:curnr = winnr()
-    if l:curnr != l:orignr
-        execute l:orignr . 'wincmd w'
+function! s:HgLog_Delete(bufnr, path)
+    if g:lawrencium_auto_close_buffers
+        call s:delete_dependency_buffers('lawrencium_diff_for', a:bufnr)
+        call s:delete_dependency_buffers('lawrencium_rev_for', a:bufnr)
     endif
-    
     " Delete the temp file if it was created somehow.
     call s:clean_tempfile(a:path)
 endfunction
@@ -1130,9 +1253,13 @@
         let l:rev = s:HgLog_GetSelectedRev()
     endif
     let l:repo = s:hg_repo()
+    let l:bufobj = s:buffer_obj()
     let l:path = l:repo.GetLawrenciumPath(b:lawrencium_logged_path, 'rev', l:rev)
+
+    " Go to the window we were in before going in the log window,
+    " and open the revision there.
     wincmd p
-    execute 'edit ' . fnameescape(l:path)
+    call s:edit_deletable_buffer('lawrencium_rev_for', l:bufobj.nr, l:path)
 endfunction
 
 function! s:HgLog_Diff(...) abort
@@ -1144,9 +1271,13 @@
         let l:revs = s:HgLog_GetSelectedRev()
     endif
     let l:repo = s:hg_repo()
+    let l:bufobj = s:buffer_obj()
     let l:path = l:repo.GetLawrenciumPath(b:lawrencium_logged_path, 'diff', l:revs)
+
+    " Go to the window we were in before going in the log window,
+    " and open the diff there.
     wincmd p
-    execute 'edit ' . fnameescape(l:path)
+    call s:edit_deletable_buffer('lawrencium_diff_for', l:bufobj.nr, l:path)
 endfunction
 
 function! s:HgLog_GetSelectedRev(...) abort
@@ -1193,7 +1324,6 @@
         setlocal filetype=hgannotate
     else
         " Store some info about the current buffer.
-        let l:cur_bufnr = bufnr('%')
         let l:cur_topline = line('w0') + &scrolloff
         let l:cur_line = line('.')
         let l:cur_wrap = &wrap
@@ -1208,10 +1338,20 @@
         setlocal scrollbind nowrap nofoldenable foldcolumn=0
         setlocal filetype=hgannotate
 
-        " When the annotated window is closed, restore the settings we
-        " changed on the current buffer.
-        execute 'autocmd BufDelete <buffer> call s:HgAnnotate_Delete(' . l:cur_bufnr . ', ' . l:cur_wrap . ', ' . l:cur_foldenable . ')'
+        " When the annotated buffer is deleted, restore the settings we
+        " changed on the current buffer, and go back to that buffer.
+        let l:annotate_buffer = s:buffer_obj()
+        call l:annotate_buffer.OnDelete('execute bufwinnr(' . l:bufnr . ') . "wincmd w"')
+        call l:annotate_buffer.OnDelete('setlocal noscrollbind')
+        if l:cur_wrap
+            call l:annotate_buffer.OnDelete('setlocal wrap')
+        endif
+        if l:cur_foldenable
+            call l:annotate_buffer.OnDelete('setlocal foldenable')
+        endif
 
+        " Go to the line we were at in the source buffer when we
+        " opened the annotation window.
         execute l:cur_topline
         normal! zt
         execute l:cur_line
@@ -1220,7 +1360,7 @@
         " Set the correct window width for the annotations.
         let l:column_count = strlen(matchstr(getline('.'), '[^:]*:')) + g:lawrencium_annotate_width_offset - 1
         execute "vertical resize " . l:column_count
-        set winfixwidth
+        setlocal winfixwidth
     endif
 
     " Make the annotate buffer a Lawrencium buffer.
@@ -1234,25 +1374,44 @@
     if g:lawrencium_define_mappings
         nnoremap <buffer> <silent> <cr> :Hgannotatediffsum<cr>
     endif
+
+    " Clean up when the annotate buffer is deleted.
+    let l:bufobj = s:buffer_obj()
+    call l:bufobj.OnDelete('call s:HgAnnotate_Delete(' . l:bufobj.nr . ')')
 endfunction
 
-function! s:HgAnnotate_Delete(orig_bufnr, orig_wrap, orig_foldenable) abort
-    execute 'call setwinvar(bufwinnr(' . a:orig_bufnr . '), "&scrollbind", 0)'
-    if a:orig_wrap
-        execute 'call setwinvar(bufwinnr(' . a:orig_bufnr . '), "&wrap", 1)'
-    endif
-    if a:orig_foldenable
-        execute 'call setwinvar(bufwinnr(' . a:orig_bufnr . '), "&foldenable", 1)'
+function! s:HgAnnotate_Delete(bufnr) abort
+    if g:lawrencium_auto_close_buffers
+        call s:delete_dependency_buffers('lawrencium_diff_for', a:bufnr)
     endif
 endfunction
 
 function! s:HgAnnotate_DiffSummary() abort
+    " Get the path for the diff of the revision specified under the cursor.
     let l:line = getline('.')
     let l:rev_hash = matchstr(l:line, '\v[a-f0-9]{12}')
+
+    " Get the Lawrencium path for the diff, and the buffer object for the
+    " annotation.
     let l:repo = s:hg_repo()
     let l:path = l:repo.GetLawrenciumPath(b:lawrencium_annotated_path, 'diff', l:rev_hash)
-    execute b:lawrencium_annotated_bufnr . 'wincmd w'
-    execute 'keepalt rightbelow split ' . fnameescape(l:path)
+    let l:annotate_buffer = s:buffer_obj()
+
+    " Find a window already displaying diffs for this annotation.
+    let l:diff_winnr = s:find_buffer_window('lawrencium_diff_for', l:annotate_buffer.nr)
+    if l:diff_winnr == -1
+        " Not found... go back to the main source buffer and open a bottom 
+        " split with the diff for the specified revision.
+        execute bufwinnr(b:lawrencium_annotated_bufnr) . 'wincmd w'
+        execute 'rightbelow split ' . fnameescape(l:path)
+        let b:lawrencium_diff_for = l:annotate_buffer.nr
+        let b:lawrencium_quit_on_delete = 1
+    else
+        " Found! Use that window to open the diff.
+        execute l:diff_winnr . 'wincmd w'
+        execute 'edit ' . fnameescape(l:path)
+        let b:lawrencium_diff_for = l:annotate_buffer.nr
+    endif
 endfunction
 
 call s:AddMainCommand("Hgannotate :call s:HgAnnotate()")