5

I would like to use the normal commands g, and g; more often to navigate through the change list.

The problem is I usually don't know how far I am in the change list from the edit I'm interested in and I have to repeat the commands many times to reach it.
Usually, what I do is :

  • use the Ex command :changes
  • hit G to go to the bottom of the change list and see the latest edits
  • look for the edit where I want to go and prepend the normal commands g, or g; with a count

To automate this process a little bit, I've come up with the following mapping:

nnoremap <leader>g, :changes<cr>G

But the G part of the mapping doesn't seem to work, the beginning of the change list is displayed (with the oldest entries), while I want to see the newest entries from the bottom.

How can I move inside vim's internal pager from a mapping ?

3

There is no way of accomplishing this by default. However, you can capture the output of the :changes command with the :redir command and paste it into a new split buffer. Here is my implementation:

command! ChangeList call <SID>ChangeList()

function! s:ChangeList() abort
    " Save z register
    let l:oldz = @z
    " Redirect any output on the command line to register z
    redir @z
    " Make the output of changes silent, though redir still captures it
    silent! changes
    redir END
    " Open scratch buffer in new split
    new
    " Insert the contents of z into current buffer silently
    silent! put z
    " Restore register z to previous value
    let @z = l:oldz
    " Remove blank lines and superfluous greater-than symbol (silently)
    silent! %g/^[\s>]*$/d
    " Go to end of file
    $
    " Make the buffer not related to any sort of file, and will never be written
    set buftype=nofile

    " Define the function MoveCursor() which will move the cursor
    " when we hit Enter on a line in the scratch buffer
    function! s:MoveCursor() abort
        " Get line number of the entry in the change list
        let l:lnum = split(getline('.'))[1]
        " Get column number of the entry in the change list
        let l:col = split(getline('.'))[2]
        " Store new cursor position inside variable l:pos
        let l:pos = [0, l:lnum, l:col, 0]
        " Close scratch buffer
        wincmd q
        " Set cursor position
        silent! call setpos('.', l:pos)
    endfunction

    " Define local mapping to call the function MoveCursor() when we hit Enter
    nnoremap <buffer> <silent> <return> :call <SID>MoveCursor()<cr>

    endfunction

" Define mapping <leader>g, to call the ChangeList() function
nnoremap <silent> <leader>g, :call <SID>ChangeList()<cr>

To move your cursor to a position stored in the change list:

  • hit <leader>g, to open a new window displaying the change list
  • move your cursor on the line showing the edit you are interested in
  • hit Enter

The window should close and your cursor should move to the line and column which were stored in the change list.

NOTE:

You can put whatever options you want after set buftype=nofile, such as set nobuflisted.

  • Thank you very much for your answer ! I hope you won't mind, but I took the liberty of making a small edit to your answer. Just adding a mapping <leader>g, to call your function, and a local mapping <return> to close the scratch buffer and move the cursor automatically in the original buffer according to the entry in the change list on which the cursor was when Enter was pressed. Of course, if you don't like the edit, feel free to delete or change it however you like. Again, thank you for the quick answer ! – saginaw Dec 21 '15 at 4:35
  • No problem! This answer was made to be quick and tweakable. – EvergreenTree Dec 21 '15 at 12:31
1

You could also try the Gundo.vim plugin for a visual representation of the undo tree.

1

You can use a plugin for this, like gundo.vim (DerW answer) or undotree. Actually undotree is better since has more customizations and realtime update.

For example, my vimrc has the following configurations for the undotree:

nnoremap <leader>tu :UndotreeToggle<CR>
let g:undotree_WindowLayout=3
let g:undotree_SetFocusWhenToggle=0
let g:undotree_TreeNodeShape='▶'
let g:undotree_DiffpanelHeight=30
0

As I noticed that there were other similar lists of files for which I wanted a navigation similar to a normal buffer (arglist, buffer list, changelist, jumplist, tag stack), I tried to adapt the answer proposed by EvergreenTree to them.

" Redir() redirects the output of an Ex command into a scratch buffer
" displayed in a new horizontal split, vertical split or tab.
" The Ex command is passed to the function with the argument a:command,
" and the destination of the output is chosen with a:where

function! s:Redir(command, where) abort

    " Begin capture inside variable l:output
    redir => l:output
    " Execute a:command (silently)
    silent! execute a:command
    " End redirection
    redir END

    if empty(l:output)
        " If nothing was captured, display a message and exit
        echom "No output"
        return
    else
        " If we open a new tab to display the capture, when we will close it,
        " we want to go back to the exact tab where we were.
        " To do so, we need to memorize the number of the current tab
        let s:lasttab = tabpagenr()

        " Execute the 2nd argument a:where silently to open a new horizontal
        " split, vertical split or tab.
        " a:where should be equal to "new", "vert new", or "tabnew"
        silent! execute a:where

        " Define a special buffer type ; it must not:
        "     - be related to a file
        "     - create a swap file
        "     - pollute the buffer list
        "     - leave any trace after being abandoned
        setlocal buftype=nofile noswapfile nobuflisted bufhidden=wipe

        " Paste the capture
        silent! put =l:output

        " If some lines of the output are too long for the screen,
        " don't wrap them because it can make them difficult to read
        setlocal nowrap

        " Remove empty lines, or a line with the single character '>'
        silent! g/^\s*$/d
        silent! g/^>$/d

        if a:command ==# "args"
            " All args are packed on a single line.
            " Format the output, so that each arg is on a separate line
            silent! keeppatterns s; ;\r;g

            " The current argument is surrounded with []
            " Because of the square brackets, when we hit Enter on its line,
            " nothing happens.
            "
            " Remove them, and put 4 spaces in front of its name, so that
            " we still have a way to distinguish it from the other arguments
            silent! keeppatterns %s;\v^\[(.*)\]$;    \1;g

            " Delete last line, because it's empty
            $delete
        endif

        " Prevent buffer to be modified, then go on last line
        setlocal nomodifiable | $
    endif

    " Check whether a:command is somewhere in s:mapping_dict ; if it's not
    " MoveCursor() can't send us anywhere when we hit Enter on a line, and
    " there's no need to create a mapping to call it
    if has_key(s:mapping_dict, a:command)

    " In the output, when we hit Enter on an entry, we want the scratch buffer
    " to be closed, and the cursor to move to the exact location of the entry.
    "
    " We will delegate this task to the MoveCursor() function.
    " For now, we just define a mapping to call it.
    "
    " MoveCursor() must know from which command is the output we are reading
    " when we hit Enter, so we must pass a:command as an argument.
    "
    " Since the mapping contains a variable (a:command) we must build the
    " mapping with a concatenation of strings and execute the result with
    " :execute
    "
    " Also we use <buffer> to make the mapping local to the scratch buffer
        execute 'nnoremap <buffer> <silent> <return>
                    \ :call <SID>MoveCursor("' . a:command . '")<cr>'

    endif

endfunction

" We want global mappings to call Redir(), passing automatically various
" Ex commands as an argument. The Ex command can be :
" :args, :buffers, :changes, :jumps, :tags
"
" The following dictionary maps a letter to each of them.
" The letter is going to be used to define a mapping of the form:
" <leader>g{letter}
let s:mapping_dict = {'args':'a', 'buffers':'b', 'changes':'c', 'jumps':'j',
            \ 'tags':'t',}

for i in range(len(s:mapping_dict))
    " Store every command and every letter from the dictionary respectively
    " in s:command and s:letter
    let [s:command, s:letter] = items(s:mapping_dict)[i]

    " For each command in the dictionary, define a mapping such as:
    " nnoremap <silent> ,gj :call <SID>Redir("jumps", "tabnew")<cr>
    execute 'nnoremap <silent> <leader>g' . s:letter .
                \ ' :call <SID>Redir("' . s:command . '", "tabnew")<cr>'
endfor

" Define the Ex command :Redir which calls the function Redir()
" Give it the attribute '-complete' with the value 'command' so that vim
" auto-completes a word typed after :Redir and suggests a command name
"
" :Redir can be useful to read the output of other system commands, from the
" command line. For example:
" :augroup, :autocmd, :command, :function
command! -nargs=1 -complete=command Redir call <SID>Redir(<q-args>, "tabnew")

" Definition of MoveCursor()
" Whenever Redir() calls MoveCursor(), it sends it the name of an Ex
" command (:args, :buffers, :changes, :jumps, :tags) as an argument.

function! s:MoveCursor(command) abort

    " Field() is a function who takes an index number as an argument (idx), and
    " returns the idx-th field of the current line
    function! s:Field(idx) abort
        return split(getline('.'))[a:idx]
    endfunction

    " The entries in every output display a buffer name in a field.
    " The index of this field varies from one command to another.
    " Same thing for the line number and the column number.
    "
    " The following dictionaries map the name of a command to the field index
    " in which a buffer name, a line number and a column number can be found.
    "
    " -1 means the command doesn't output any buffer name / line number /
    "  colomn number
    let [l:buffer_idx_dict, l:line_idx_dict, l:col_idx_dict] =
                \ [{"args":0, "buffers":0, "changes":-1, "jumps":3, "tags":4,},
                \ {"args":-1, "buffers":-1, "changes":1, "jumps":1, "tags":3,},
                \ {"args":-1, "buffers":-1, "changes":2, "jumps":2, "tags":-1,}]

    " In those 3 dictionaries, look up the field index for a:command
    " and store them in 3 variables:
    " l:buffer_field, l:line_field and l:col_field
    let [l:buffer_idx, l:line_idx, l:col_idx] =
                \ [l:buffer_idx_dict[a:command],
                \ l:line_idx_dict[a:command],
                \ l:col_idx_dict[a:command]]

    " If the index is different from -1, we can call Field() to retrieve the
    " content of a specific field:
    " a buffer name, a line number, a column number
    "
    " We store those informations in 3 variables:
    " l:bname, l:lnum, l:cnum
    if l:buffer_idx != -1 | let l:bname = <SID>Field(l:buffer_idx) | endif
    if l:line_idx != -1 | let l:lnum = <SID>Field(l:line_idx) | endif
    if l:col_idx != -1 | let l:cnum = <SID>Field(l:col_idx) | endif

    " In the output of :tags, the file name is displayed with a relative path;
    " example: intro.txt
    " We need an absolute path; example: /usr/share/vim/vim74/doc/intro.txt
    if a:command ==# 'tags' |
                \ let l:bname = glob('$VIMRUNTIME') . '/doc/' . l:bname | endif

    " Store new cursor position inside variable l:pos
    if exists('l:lnum') && exists('l:cnum') |
                \ let l:pos = [0, l:lnum, l:cnum, 0] |
                \ elseif exists('l:lnum') |
                \ let l:pos = [0, l:lnum, 0, 0] |
                \ endif

    " Close scratch buffer
    wincmd q

    " Go back to the last tab we were before opening scratch buffer
    execute 'tabnext ' . s:lasttab

    " If l:bname exists and:
    " it matches an existing file, or a:command is 'buffers',
    " then load it in the current window.
    "
    " Need to check if bname exists because there could be no buffer name,
    " e.g.: entries in the jumplist whose motion stays in the current buffer.
    " Need to check if bname matches a file because sometimes it's just
    " a random string of text (e.g. some entries in the jumplist and in the
    " tag stack).
    " If a:command = 'buffers', then bname is a number not a filename, so
    " the previous check will fail but we do want the buffer to be loaded
    " anyway.

    if exists('l:bname') && ( !empty(glob(l:bname)) || a:command ==# 'buffers' )

        " We could use :sbuffer instead of :buffer because
        "     if &switchbuf = useopen,usetab
        " and if l:bname is already displayed in another window or tab,
        " then instead of loading the buffer in the current window, the focus
        " would be switched to wherever the buffer is currently displayed.
        "
        " Problem: if the buffer is not displayed, :sbuffer opens a new split
        " instead of loading the buffer in the current window
        " Do we want that ?

        execute 'buffer ' . l:bname
    endif

    " Set cursor position
    if exists('l:pos') | silent! call setpos('.', l:pos) | endif

    " Fold everything except where we are
    normal zMzv
    " Set current line as the middle of the screen
    normal zz

endfunction

I didn't test it much but so far it seems to work.
It defines 5 mappings (<leader>ga, <leader>gb, <leader>gc, <leader>gj, <leader>gt) to navigate through the arglist, buffer list, changelist, jumplist and tagstack.

The current behavior is to display the buffer described in the entry of a list in the current window, but I wonder if it wouldn't be better to first check whether the buffer is already displayed in another one. If it is, the focus should then be given to it instead of displaying the buffer in a 2nd window.

But I don't know how to do that.

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.