Tuesday, October 15, 2013

Vim: Distinguish a location-list from a quickfix-list

Strange that there is no easy way (that I know of) to differentiate a location-list from a quickfix-list. Today I noticed that the buffer-list (:ls) displays a different name for each type, and thus:

fun! GetBufferListOutputAsOneString()
    " Return the ':ls' output as one string.  Call it with ':silent'
    " to suppress the normal ls output.
    let buffer_list = ''
    redir =>> buffer_list
    redir END
    return buffer_list

fun! IsLocationListBuffer()
    " Return 1 if the current buffer contains a location list.
    " Return 0 otherwise.
    if &ft != 'qf'
        return 0

    silent let buffer_list = GetBufferListOutputAsOneString()

    let l:quickfix_match = matchlist(buffer_list,
                \ '\n\s*\(\d\+\)[^\n]*Quickfix List')
    if empty(l:quickfix_match)
        " no match and ft==qf: current buffer contains a location list
        return 1
    let quickfix_bufnr = l:quickfix_match[1]
    return quickfix_bufnr == bufnr('%') ? 0 : 1

Saturday, July 6, 2013

Conflict Resolution with Vim Conflict Slides

The Conflict Slides Vim plugin allows you to lock to a conflict range and replace that whole range with individual sections inside the conflict markers.  This has several advantages.  For example, you are independent from how the other buffers are named and because modifications to the buffer are fully controlled, you gain some safety while exploring the conflict.

I recently added display helper functions that allow you to toggle the diff option in selected windows.  That in turn makes it possible to see 2-way diffs at different points of the conflict resolution process.  I am going to show you how such a procedure could look like.

There is more documentation on the Conflict Slides frontpage and the functions are documented in more detail in the plugin file.

The tutorial part focuses on Git somewhat, but as long as you have the same conflict markers, this should work with any VCS.


I would recommend to turn the folding off in the post-lock callback to mitigate jolting when changing content.  You can turn the folding back on with the argument 'on' instead of 'off', and the folding can be toggled with the argument 'toggle'.

fun! g:conflict_slides_post_lock_callback()
    call CS_DiffChangeFolding('off')

If your VCS offers to insert the base content into the markers, you should consider to turn that on. Otherwise the base slide is not available.  In Git you can add the base content with the following command:

git config merge.conflictstyle diff3

Generally, I am using three diff windows with ours, dest, theirs content.  That is the setup provided by vim-fugitive.  You should really check out that plugin, if you haven't done so already.  It is tremendously easy to find the commit for a line in a conflict (among many other things).

Example Mappings

Here are some mappings that extend the default mappings from Conflict Slides (CS) a bit.  You can use this as a template for your own mappings.  The ^[1-3] mappings do essentially turn the diff on in all 3 windows, except in the given window where the diff is toggled.  This allows you to see a two-way diff for any combination of the three windows, as well as return to a three-way diff.

nnoremap <silent> ^l :call CS_LockNextConflict('lock-current')<CR>zz
nnoremap <silent> ^n :call CS_LockNextConflict()<CR>zz
nnoremap <silent> ^N :call CS_LockNextConflict('backward')<CR>zz

nnoremap <silent> ^u :call CS_ReleaseLockedConflict()<Bar>
                     \ call CS_DiffChangeFolding('on')<CR>

nnoremap <silent> ^f :call CS_DiffChangeFolding('toggle')<CR>zz

nnoremap <silent> ^1 :call CS_DiffSwitch3Toggle(1)<CR>
nnoremap <silent> ^2 :call CS_DiffSwitch3Toggle(2)<CR>
nnoremap <silent> ^3 :call CS_DiffSwitch3Toggle(3)<CR>

You can simply define a g:conflictslides_locked_mappings dictionary in your Vim configuration to override the one from CS.  The top section in the following snippet is the same as in the plugin.  Below that are a few mappings that allow you to insert a slide while turning diff on/off in selected windows.  The first two of those mappings are useful immediately after you locked to a conflict to explore the changes in each parent.

let g:conflictslides_locked_mappings = {
        \ 'b' : ":call CS_ModifyConflictContent('base')<CR>",
        \ 'B' : ":call CS_ModifyConflictContent('base', 'append')<CR>",
        \ 'o' : ":call CS_ModifyConflictContent('ours')<CR>",
        \ 'O' : ":call CS_ModifyConflictContent('ours', 'append')<CR>",
        \ 't' : ":call CS_ModifyConflictContent('theirs')<CR>",
        \ 'T' : ":call CS_ModifyConflictContent('theirs', 'append')<CR>",
        \ 'a' : ":call CS_ModifyConflictContent('ours theirs')<CR>",
        \ 'A' : ":call CS_ModifyConflictContent('theirs ours')<CR>",
        \ 'f' : ":call CS_ModifyConflictContent('forward')<CR>",
        \ 'r' : ":call CS_ModifyConflictContent('reverse')<CR>",
        \ 'F' : ":call CS_ModifyConflictContent('forward-nobase')<CR>",
        \ 'R' : ":call CS_ModifyConflictContent('reverse-nobase')<CR>",
        \ 'e' : ":call CS_ReleaseLockedConflict()<CR>",
        \ 'q' : ":call CS_ModifyConflictContent('forward')<Bar>"
        \               . "call CS_ReleaseLockedConflict()<CR>",
        \ '<CR>' : ":call CS_LockNextConflict()<CR>",
        \ '<BS>' : ":call CS_LockNextConflict('restore-conflict')<CR>",
        \ 'V' : ":call CS_SelectCurrentConflictRange(0)<CR>",
        \ 'v' : ":call CS_SelectCurrentConflictRange(500)<CR>",
        \ '<space>b'        : ":call CS_ModifyConflictContent('base')<Bar>"
        \                           . "call CS_DiffSwitch3Off(3)<CR>",
        \ '<space><space>b' : ":call CS_ModifyConflictContent('base')<Bar>"
        \                           . "call CS_DiffSwitch3Off(1)<CR>",
        \ '<space>o'        : ":call CS_ModifyConflictContent('ours')<Bar>"
        \                           . "call CS_DiffSwitch3Off(1)<CR>",
        \ '<space>t'        : ":call CS_ModifyConflictContent('theirs')<Bar>"
        \                           . "call CS_DiffSwitch3Off(3)<CR>",
        \ '<space>a'        : ":call CS_DiffSwitch3AllOn()<CR>",
        \ '<space>1'        : ":call CS_DiffSwitch3Toggle(1)<CR>",
        \ '<space>2'        : ":call CS_DiffSwitch3Toggle(2)<CR>",
        \ '<space>3'        : ":call CS_DiffSwitch3Toggle(3)<CR>",
        \ '<space>f'        : ":call CS_DiffChangeFolding('on')<CR>zz",
        \ '<space>F'        : ":call CS_DiffChangeFolding('off')<CR>zz",
        \ }

There is nothing in there that cannot be done with the global mappings from above.  So, it's just a template in case you want to create more convenient mappings with the mechanism provided by CS.  You may need to adapt the window number in the calls if you use a different setup.

Prepare for Conflict

There is this awesome merge resolution boot camp with solutions and I've picked one merge to demonstrate how to resolve a conflict.  You can follow if you like.  This is the commit:

736a2dd Merge tag 'virtio-next-for-linus' of git://...

First, prepare two tags and a branch.

git tag ExpDest 736a2dd2571ac56b11ed95a7814d838d5311be04
git tag ExpTheirs ExpDest^2
git checkout -b ExpMaster ExpDest^1

Inspect the result with this command:

git log --graph --oneline --decorate --simplify-by-decoration -3 ExpDest

It should look similar to this:

Now, let's try to merge.

git merge --log ExpTheirs

This should result in a conflict with several unmerged files.


If you use Git without Fugitive, you can start the setup described above by adding the following section to your ~/.gitconfig file,

[mergetool "gv3"]
cmd = gvim -f -d  \"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"+2wincmd w\"

and start that mergetool for drivers/char/virtio_console.c like so:

git mergetool -t gv3 drivers/char/virtio_console.c

By pressing ^l you can lock to the one and only conflict in this file.  This will produce a beautiful rainbow of colors.

The initial 3-way diff of the conflict

Now you can use the <space>b and <space><space>b mappings to see two-way diffs between the base slide and ours/theirs respectively.  (The same can be accomplished with b and then ^1 or ^3)

Difference ours/base

Difference base/theirs

Aha!  The conditional branch has been wrapped in a spinlock on our side and a very important call has been replaced by an even more important call on their side.  If you are using Fugitive you can very conveniently open a blame view for their commit and try to find out more about that change.

Fugitive's blame view started from their modification

To resolve it you can take our side with <space>o, and append their side with T.

Spotting the surplus line in the difference dest/theirs

Because this is a two-way diff with theirs, the wrong call can be easily spotted.  Let's unlock with e (for edit) and delete the wrong line.  You can visualize the changes on each side by using the mappings ^1 and ^3.

Resolved difference ours/dest
Resolved difference dest/theirs

Resolve more

If you would like to resolve some more conflicts yourself, search the repository with the following command:

git  log  --merges  --grep Conflicts  master

Monday, May 20, 2013

Powerful insert mode completion configuration for Vim paired with AutoComplPop

Combine results of multiple sources for completion into one menu that pops up automatically. To keep the noise low you can add to an open Popup Menu at different leading word lengths and you will be able to complete matches with the same key (default: Tab) in forward and backward menus (e.g.: <C-P>).

The most interesting part is the localcomplete#localMatches function which allows you to scan an area around the current line (up to the whole buffer) and configure how to sort matches. My favorite sort order starts at the current line and alternates matches from above and below. You can combine local completion with omni completions like RopeOmni for Python.

Local, Rope, and all-buffers completion combined

Now a little disclaimer. This is partially hacking and you might have to wrestle with some quirks. Vim's Popup Menu and completion functions aren't easily accessible. However, I mostly got what I wanted and it is actually a considerable improvement to my workflow. Give it a try.  :D


Generally the scheme in all this is that there are never items preselected in the menu. That is also the most useful way to compare omni completion methods. In addition to what follows, you can still configure completion methods and compare results through the explicit triggers <C-X><C-U> and <C-X><C-O>.
autocmd FileType python setlocal omnifunc=RopeOmni
autocmd FileType python setlocal completefunc=jedi#complete

Beware of the value longest in the completeopt Vim setting. I don't use it, but I've noticed that it conflicts with the expected behavior.

If you don't know much about insert mode completion, reading *ins-completion* in the Vim docs will help you understand some of the terms (and restrictions of the implementation) that follow.

AutoComplPop is used to trigger the Popup Menu. I did not look much at YouCompleteMe. ACP looked like it was what I needed. In particular, you can pretty much configure exactly what will happen for each individual file type. Nevertheless, you will probably be able to use the completion functions with YCM.


The first thing you need is a patched AutoComplPop. Please read the commit messages on top of the fork-point to see what changed. I also updated the documentation in the fork.

The most notable change is that you can configure the keys used to browse the menu. Furthermore the colors and mappings are different for reverse (e.g. <C-P>) and forward menus. This gives you a clue where to look for the most likely completion, and you can always use the same key for completion. It is possible to turn off the mapping switch, and you can configure the same color for both directions.

When you use the switch (the default) you need to include a menu adaption into insert mode completion mappings. I tried to avoid it but when done inside ACP, the previous color was always flickering through.

Here some examples. The number in the call is the direction. One is forward, and two is backward.
" Filename completion
inoremap <C-F> <C-R>=acp#pum_color_and_map_adaptions(1)<CR><C-X><C-F><C-P>

" Spelling corrections
inoremap <C-S> <C-R>=acp#pum_color_and_map_adaptions(1)<CR><C-X><C-S><C-P>

" Omni completion
inoremap <C-O> <C-R>=acp#pum_color_and_map_adaptions(1)<CR><C-X><C-O><C-P>

" User defined completion
inoremap <C-U> <C-R>=acp#pum_color_and_map_adaptions(1)<CR><C-X><C-U><C-P>

" Switch to keyword completion backwards
inoremap <C-L>  <C-R>=acp#pum_color_and_map_adaptions(2)
            \ <CR><C-R>=pumvisible()
            \ ? "\<lt>C-E>\<lt>C-P>\<lt>C-N>"
            \ : "\<lt>C-P>"<CR>

" Close the pum with <Return>.  Otherwise, it would restore the old value after
" manual completion.
inoremap <Return> <C-R>=pumvisible()
            \ ? "\<lt>C-X>\<lt>Return>"
            \ : "\<lt>Return>"<CR>

" Allow to exit the pum with Up/Down
inoremap <Up>  <C-R>=pumvisible() ? "\<lt>C-X>\<lt>Up>" : "\<lt>Up>"<CR>
inoremap <Down>  <C-R>=pumvisible() ? "\<lt>C-X>\<lt>Down>" : "\<lt>Down>"<CR>

A few hints for the ACP configuration.  See the ACP doc for the actual configuration help.
" Remove dictionary lookup from the Vim keyword completion.  It did always
" complete the first match for me.  If you edit files with tags you might
" want to add those.
let g:acp_completeOption = '.,w,b'

" How keyword completion is triggered.  Usually you want variables before
" the current line.  ... Unless you write a file bottom up, that is.
let g:acp_behaviorKeywordCommand = "\<C-P>"

" This adds the local-completion function before all other completions
" (snipmate, keyword, file) in the g:acp_behavior records set up by ACP.
let g:acp_behaviorUserDefinedFunction = 'localcomplete#localMatches'
let g:acp_behaviorUserDefinedMeets = 'acp#meetsForKeyword'

And templates for some new options. All given values are the defaults
" Highlights to use for the pop up menu.
let g:acp_colorForward = 'AutoComplPopColorDefaultForward'
let g:acp_colorReverse = 'AutoComplPopColorDefaultReverse'

" Specify the keys used to select entries.  These create insert mode
" mappings like you know them from the Vim documentation.
let g:acp_nextItemMapping = ['<TAB>', '\<lt>TAB>']
let g:acp_previousItemMapping = ['<S-TAB>', '\<lt>S-TAB>']

" Turn the reverse mapping switch on/off:
let g:acp_reverseMappingInReverseMenu = 1

" Don't preselect the first item.  Use the same key to select and to browse
let g:acp_autoselectFirstCompletion = 0


The localcomplete plugin tries to replicate some built-in Vim completions so that they can be combined and configured.

Of interest are essentially the following two files:


Here you find three completion functions:

This is the function mentioned above. It searches through the configured area of the current buffer only. All of the configuration in the first section of the source file applies to it. Please read through that section. There is no documentation file.

This function searches through all buffers. It respects the case and keyword-char configuration. It's just an unsophisticated replacement for the functionality provided by Vim.

Search the dictionary configured in Vim's 'dictionary' setting for matches. All matches are done case-sensitively, because otherwise all the names in the dictionary would come first. It is a very simple function. The dictionary has to be utf-8 encoded.

All three functions can have individual minimum leading word lengths configured after which they start to produce results. This can be used to keep the number of matches in the menu reasonable. For example, you might want to add local-matches after two characters, but not the masses of dictionary matches at that point. The variables for that are described in the second section of this file.


This is a pretty rough and hardcoded module for demonstration purposes.

It contains a function to combine multiple completion functions into one. ACP can be configured to try several completion functions in a row until one generates results, but it is much more useful to have multiple results combined into one menu. Here is a client function that calls it with the three completion functions from above.
function combinerEXP#completeCombinerTextish(findstart, keyword_base)
    let l:all_completers = [
                \ 'localcomplete#localMatches',
                \ 'localcomplete#allBufferMatches',
                \ 'localcomplete#dictMatches',
                \ ]
    return combinerEXP#completeCombinerABSTRACT(
                \ a:findstart,
                \ a:keyword_base,
                \ l:all_completers
                \ 0)
The zero is an index into l:all_completers to select the function used in the findstart mode. Because of the way completion functions interact with Vim, there can be only one findstart column that is shared by all combined completion functions.

There is a Python combiner that addresses requirements when combining other completion functions with RopeOmni from python-mode. If you are interested in rope completions, you should take a closer look at this file. Here is an example combiner:
function combinerEXP#completeCombinerPython(findstart, keyword_base)
    let l:before_rope = []
    let l:after_rope = [
                \ 'localcomplete#localMatches',
                \ 'localcomplete#allBufferMatches',
                \ ]
    return combinerEXP#ropeCombiner(
                \ a:findstart,
                \ a:keyword_base,
                \ l:before_rope,
                \ l:after_rope,
                \ 0)
It makes sense to prepend local-completion with a very slim search area. The all-buffers completion searches the current buffer first, and so you can fall back to that completion for all matches in the current buffer. More on that below.

Configuring Content

Alright. Now we can connect those two plugins and get a lot of opportunities for configuration. You can specify the completion for each &filetype individually by filling in the g:acp_behavior variable.

For each filetype you can configure multiple entries, that are tried in turn until one generates matches. The following example creates the entry for all files that don't have a filetype specific entry (e.g. an 'xml' key instead of the '*'). This one tries the text combiner first, and falls back to keyword completion in reverse mode.
if ! exists("g:acp_behavior")
      let g:acp_behavior = {
        \ '*' : [
        \     {
        \       'completefunc': 'combinerEXP#completeCombinerTextish',
        \       'command': "\<C-X>\<C-U>",
        \       'meets': 'acp#meetsForKeyword',
        \       'repeat': 1,
        \     },
        \     {
        \       'command': "\<C-P>",
        \       'meets': 'acp#meetsForKeyword',
        \       'repeat': 0,
        \     },
        \ ],

Here are the two completions side by side.

Localcomplete and standard keyword completion side-by-side (the sorting is different)

Now let's configure at what point which entries appear.

It is important to note that ACP has a minimum leading word requirement, as well as the completion functions from localcomplete itself. ACP won't open the menu unless the minimum length is reached. These values are used in the corresponding 'meets' functions. You can see one of those above (acp#meetsForKeyword). You can write your own meets functions. They are called with the line content before the cursor position and are supposed to return non-zero if the requirements for opening the Popup Menu are met.
" count of chars required to start keyword completion
let g:acp_behaviorKeywordLength = 2

If you have multiple completion functions from the localcomplete plugin combined into one, you can configure individual minimal lengths of the word before the cursor. There is a catch, however. The Popup menu has to be updated to display the new content. You can do that after every typed character, but you'll soon notice some lag when combined with expensive omni completions. Therefore it is possible to configure particular word-lengths at which the menu should be updated. You can simply use the minimum length variables for that:
let g:localcomplete#LocalMinPrefixLength = 1
let g:localcomplete#AllBuffersMinPrefixLength = 1
let g:localcomplete#DictMinPrefixLength = 5

let g:acp_refeed_checkpoints = [
            \ g:localcomplete#LocalMinPrefixLength,
            \ g:localcomplete#AllBuffersMinPrefixLength,
            \ g:localcomplete#DictMinPrefixLength]

" Beware. Probably expensive (flickering)
"let g:acp_refeed_after_every_char = 0
In the same way you can transmit the additional keyword configuration to ACP:
let g:localcomplete#AdditionalKeywordChars = '-'
let g:acp_keyword_chars_for_checkpoint =
            \ g:localcomplete#AdditionalKeywordChars

Python Configuration

Here is part of the Python configuration I am playing with right now. A very slim area is searched before RopeOmni has its turn. I think it has a better chance of putting local variables in the right order.
let b:LocalCompleteLinesAboveToSearchCount = 15
let b:LocalCompleteLinesBelowToSearchCount = 10
The combiner for it looks like this:
function! g:localFirstPythonCombiner(findstart, keyword_base)
    let l:before_rope = [
                \ 'localcomplete#localMatches',
                \ ]
    let l:after_rope = [
                \ 'localcomplete#allBufferMatches',
                \ ]
    return combinerEXP#ropeCombiner(
                \ a:findstart,
                \ a:keyword_base,
                \ l:before_rope,
                \ l:after_rope,
                \ 0)
We need to tell ACP when to add entries to the menu. This is really lagging when done after every typed character.
" Minimum leading word lengths
let b:LocalCompleteLocalMinPrefixLength = 3
let b:LocalCompleteAllBuffersMinPrefixLength = 5

" Restart omni completion after these word lengths.
let b:acp_refeed_checkpoints = [
            \ b:LocalCompleteLocalMinPrefixLength,
            \ b:LocalCompleteAllBuffersMinPrefixLength,
            \ ]

" Preemptively override global values
let b:acp_refeed_after_every_char = 0
let b:LocalCompleteAdditionalKeywordChars = ''
let b:acp_keyword_chars_for_checkpoint =
            \ b:LocalCompleteAdditionalKeywordChars
In the RopeOmni version that I used is a bug where keywords override object attributes. So, for example, when you add local-matches after two characters, and an object attribute starts with either of 'is in as', all initially good rope results are removed from the menu. Therefore I would try to avoid using only small checkpoints without a bigger one.

You can add all this in a ftplugin/python.vim file so that it is nicely grouped. Here is the ACP configuration for Python that uses the combiner from above:
let g:acp_behavior['python'] = [
        \     {
        \       'command': "\<C-X>\<C-U>",
        \       'completefunc': 'g:localFirstPythonCombiner',
        \       'meets': 'acp#meetsForPythonOmni',
        \       'repeat': 1,
        \     },
The repeat dictionary key is important for omni-completions that can give you info about, for example, function parameters after typing an opening parenthesis. It tries to restart the completion menu when it closes. If that doesn't work it's probably the fault of the meets function. Take a look at the ones defined in ACP if you want to write your own.

The startup word length for this particular meets function can be set with the following variable. I've added dots and opening parentheses to that function so that a setting of 1 will give you maximal info about objects while not always bothering you.
let g:acp_behaviorPythonOmniLength = 1

Display function parameters and default arguments after an opening parenthesis

RopeOmni needs a syntactically correct file. By default you see that the cursor becomes red when RopeOmni errors are caught in ropeCombiner. Anyway, the other completions can jump in. And with the error silencing done in the combiner it is actually a quiet good and fun completion.

Prefer surrounding lines over Rope's object attributes


Snipmate has the Tab key hardcoded, but you can easily find and change it in after/plugin/snipMate.vim.

This is the standard entry that gets normally added if you don't override entries in g:acp_behavior:
{ 'meets': 'acp#meetsForSnipmate',
\ 'completefunc': 'acp#completeSnipmate',
\ 'onPopupClose': 'acp#onPopupCloseSnipmate',
\ 'repeat': 0, 
\ 'command': "\<C-X>\<C-U>"},
I would recommend to put it first and I think it works best when the length limit is lower than the others:
let g:acp_behaviorSnipmateLength = 1
Only triggers that start with an uppercase character end up in the Popup Menu and you have to explicitly select the completion for it to expand (default: <C-Y>)


Now the sad part. ACP is a strange beast. There is no real access to Vim's PopUp Menu and this is all done by streaming a bunch of key presses to Vim. I don't understand everything that is happening sometimes, but here a few tips:

For the localcomplete functions, always set repeat to 1 (or let it unspecified). If the updates at checkpoints don't work, this might be the cause.  However, the completion is quiet persistent in that case.  If you don't like that, try to increase or decrease the checkpoint by one.
Another alternative in this case is to create mappings that turn the auto-completion on/off:
map <S-F5> :AcpDisable<CR>
map <F5>   :AcpEnable<CR>

If you want to trigger built-in commands, I would recommend to set repeat to 0 explicitly. Otherwise, if you use checkpoints, the completion is undone to the point where the PopUp Menu was opened.

ACP prepares individual records for the following filetypes: ruby, python, perl, xml, html, xhtml, css, javascript, coffee, ls.
This can be a bit surprising if you edit any of such files infrequently. The most failsafe way to fall back to the default behavior is probably to add an autocommand like this:
autocmd FileType ruby call remove(g:acp_behavior, "ruby")

I hope I didn't break too much for you, but I am done experimenting with it. It would be awesome if there could be a more sophisticated access to Vim's PopUp Menu. That would make things a lot easier, I guess.

But don't let all this discourage you. I am actually quiet happy with it. In particular in Python.

Give <C-P> some relief

Happy playing around with the word lengths. There must be some optimal adjustment.

(The snippets from the source code are under LGPLv3)

Sunday, April 21, 2013

Little helpers

Do you sometimes prepare a commit series, look through it again, and find some small changes like doc improvements or name changes that you need to squash into the last commit to that file?  This little snippet creates a fixup commit for each dirty file tracked by Git (it predates the --fixup option somewhat :).


err() {
  echo "Error: $@" >&2

git rev-parse --is-inside-work-tree &>/dev/null || err "Not inside a work tree"
[ -z "$(git diff --staged --name-only)" ] || err "Stage is not empty"

for dirtyfile in $(git diff --name-only) ; do
  commit_message=$(git log --oneline -- "$dirtyfile" | head -1 | sed 's/^\w* //g')
  git commit -m "fixup! ${commit_message}" -- "${dirtyfile}"

These are the sort of tools that are unlikely to be shipped with a VCS, but would fit nicely into a workbench tool-shed.  Of course, there can be much more sophisticated tools as well as whole toolsets describing workflows.