" VIM plugin for doing single, multi-patch or diff code reviews {{{ " Home: http://www.vim.org/scripts/script.php?script_id=1563 " Version : 0.2.2 "{{{ " Author : Manpreet Singh < junkblocker@yahoo.com > " Copyright : 2006-2010 by Manpreet Singh " License : This file is placed in the public domain. " No warranties express or implied. Use at your own risk. " " Changelog : " " 0.2.2 - Security fixes by removing custom tempfile creation " - Removed need for DiffReviewCleanup/PatchReviewCleanup " - Better command execution error detection and display " - Improved diff view and folding by ignoring modelines " - Improved tab labels display " " 0.2.1 - Minor temp directory autodetection logic and cleanup " " 0.2 - Removed the need for filterdiff by implementing it in pure vim script " - Added DiffReview command for reverse (changed repository to " pristine state) reviews. " (PatchReview does pristine repository to patch review) " - DiffReview does automatic detection and generation of diffs for " various Source Control systems " - Skip load if VIM 7.0 or higher unavailable " " 0.1 - First released "}}} " Documentation: "{{{ " =========================================================================== " This plugin allows single or multiple, patch or diff based code reviews to " be easily done in VIM. VIM has :diffpatch command to do single file reviews " but a) can not handle patch files containing multiple patches or b) do " automated diff generation for various version control systems. This plugin " attempts to provide those functionalities. It opens each changed / added or " removed file diff in new tabs. " " Installing: " " For a quick start, unzip patchreview.zip into your ~/.vim directory and " restart Vim. " " Details: " " Requirements: " " 1) VIM 7.0 or higher built with +diff option. " " 2) A gnu compatible patch command installed. This is the standard patch " command on Linux, Mac OS X, *BSD, Cygwin or /usr/bin/gpatch on newer " Solaris. " " 3) Optional (but recommended for speed) " " Install patchutils ( http://cyberelk.net/tim/patchutils/ ) for your " OS. For windows it is available from Cygwin " " http://www.cygwin.com " " or GnuWin32 " " http://gnuwin32.sourceforge.net/ " " Install: " " 1) Extract the zip in your $HOME/.vim or $VIM/vimfiles directory and " restart vim. The directory location relevant to your platform can be " seen by running :help add-global-plugin in vim. " " 2) Restart vim. " " Configuration: " " Optionally, specify the locations to these filterdiff and patch commands " and location of a temporary directory to use in your .vimrc. " " let g:patchreview_patch = '/path/to/gnu/patch' " " " If you are using filterdiff " let g:patchreview_filterdiff = '/path/to/filterdiff' " " " Usage: " " Please see :help patchreview or :help diffreview for details. " ""}}} " Enabled only during development " unlet! g:loaded_patchreview " DEBUG " unlet! g:patchreview_patch " DEBUG " unlet! g:patchreview_filterdiff " DEBUG " let g:patchreview_patch = 'patch' " DEBUG if v:version < 700 finish endif if ! has('diff') call confirm('patchreview.vim plugin needs (G)VIM built with +diff support to work.') finish endif " load only once if (! exists('g:patchreview_debug') && exists('g:loaded_patchreview')) || &compatible finish endif let g:loaded_patchreview="0.2.2" let s:msgbufname = '-PatchReviewMessages-' function! Debug(str) "{{{ if exists('g:patchreview_debug') Pecho 'DEBUG: ' . a:str endif endfunction command! -nargs=+ -complete=expression Debug call s:Debug() "}}} function! PR_wipeMsgBuf() "{{{ let winnum = bufwinnr(s:msgbufname) if winnum != -1 " If the window is already open, jump to it let cur_winnr = winnr() if winnr() != winnum exe winnum . 'wincmd w' exe 'bw' exe cur_winnr . 'wincmd w' endif endif endfunction "}}} function! Pecho(...) "{{{ " Usage: Pecho(msg, [return_to_original_window_flag]) " default return_to_original_window_flag = 0 " let cur_winnr = winnr() let winnum = bufwinnr(s:msgbufname) if winnum != -1 " If the window is already open, jump to it if winnr() != winnum exe winnum . 'wincmd w' endif else let bufnum = bufnr(s:msgbufname) if bufnum == -1 let wcmd = s:msgbufname else let wcmd = '+buffer' . bufnum endif exe 'silent! botright 5split ' . wcmd endif setlocal modifiable setlocal buftype=nofile setlocal bufhidden=delete setlocal noswapfile setlocal nowrap setlocal nobuflisted if a:0 != 0 silent! $put =a:1 endif exe ':$' setlocal nomodifiable if a:0 > 1 && a:2 exe cur_winnr . 'wincmd w' endif endfunction command! -nargs=+ -complete=expression Pecho call s:Pecho() "}}} function! PR_checkBinary(BinaryName) "{{{ " Verify that BinaryName is specified or available if ! exists('g:patchreview_' . a:BinaryName) if executable(a:BinaryName) let g:patchreview_{a:BinaryName} = a:BinaryName return 1 else Pecho 'g:patchreview_' . a:BinaryName . ' is not defined and ' . a:BinaryName . ' command could not be found on path.' Pecho 'Please define it in your .vimrc.' return 0 endif elseif ! executable(g:patchreview_{a:BinaryName}) Pecho 'Specified g:patchreview_' . a:BinaryName . ' [' . g:patchreview_{a:BinaryName} . '] is not executable.' return 0 else return 1 endif endfunction "}}} function! ExtractDiffsNative(...) "{{{ " Sets g:patches = {'reason':'', 'patch':[ " { " 'filename': filepath " 'type' : '+' | '-' | '!' " 'content' : patch text for this file " }, " ... " ]} let g:patches = {'reason' : '', 'patch' : []} " TODO : User pointers into lines list rather then use collect if a:0 == 0 let g:patches['reason'] = "ExtractDiffsNative expects at least a patchfile argument" return endif let patchfile = expand(a:1, ':p') if a:0 > 1 let patch = a:2 endif if ! filereadable(patchfile) let g:patches['reason'] = "File " . patchfile . " is not readable" return endif unlet! filterdiffcmd let filterdiffcmd = '' . g:patchreview_filterdiff . ' --list -s ' . patchfile let fileslist = split(system(filterdiffcmd), '[\r\n]') for filewithchangetype in fileslist if filewithchangetype !~ '^[!+-] ' Pecho '*** Skipping review generation due to unknown change for [' . filewithchangetype . ']' continue endif unlet! this_patch let this_patch = {} unlet! relpath let relpath = substitute(filewithchangetype, '^. ', '', '') let this_patch['filename'] = relpath if filewithchangetype =~ '^! ' let this_patch['type'] = '!' elseif filewithchangetype =~ '^+ ' let this_patch['type'] = '+' elseif filewithchangetype =~ '^- ' let this_patch['type'] = '-' endif unlet! filterdiffcmd let filterdiffcmd = '' . g:patchreview_filterdiff . ' -i ' . relpath . ' ' . patchfile let this_patch['content'] = split(system(filterdiffcmd), '[\n\r]') let g:patches['patch'] += [this_patch] Debug "Patch collected for " . relpath endfor endfunction "}}} function! ExtractDiffsPureVim(...) "{{{ " Sets g:patches = {'reason':'', 'patch':[ " { " 'filename': filepath " 'type' : '+' | '-' | '!' " 'content' : patch text for this file " }, " ... " ]} let g:patches = {'reason' : '', 'patch' : []} " TODO : User pointers into lines list rather then use collect if a:0 == 0 let g:patches['reason'] = "ExtractDiffsPureVim expects at least a patchfile argument" return endif let patchfile = expand(a:1, ':p') if a:0 > 1 let patch = a:2 endif if ! filereadable(patchfile) let g:patches['reason'] = "File " . patchfile . " is not readable" return endif call s:PR_wipeMsgBuf() let collect = [] let linum = 0 let lines = readfile(patchfile) let linescount = len(lines) State 'START' while linum < linescount let line = lines[linum] let linum += 1 if State() == 'START' let mat = matchlist(line, '^--- \([^\t]\+\).*$') if ! empty(mat) && mat[1] != '' State 'MAYBE_UNIFIED_DIFF' let p_first_file = mat[1] let collect = [line] Debug line . State() continue endif let mat = matchlist(line, '^\*\*\* \([^\t]\+\).*$') if ! empty(mat) && mat[1] != '' State 'MAYBE_CONTEXT_DIFF' let p_first_file = mat[1] let collect = [line] Debug line . State() continue endif continue elseif State() == 'MAYBE_CONTEXT_DIFF' let mat = matchlist(line, '^--- \([^\t]\+\).*$') if empty(mat) || mat[1] == '' State 'START' let linum -= 1 continue Debug 'Back to square one ' . line() endif let p_second_file = mat[1] if p_first_file == '/dev/null' if p_second_file == '/dev/null' let g:patches['reason'] = "Malformed diff found at line " . linum return endif let p_type = '+' let filepath = p_second_file else if p_second_file == '/dev/null' let p_type = '-' let filepath = p_first_file else let p_type = '!' let filepath = p_first_file endif endif State 'EXPECT_15_STARS' let collect += [line] Debug line . State() elseif State() == 'EXPECT_15_STARS' if line !~ '^*\{15}$' State 'START' let linum -= 1 Debug line . State() continue endif State 'EXPECT_CONTEXT_CHUNK_HEADER_1' let collect += [line] Debug line . State() elseif State() == 'EXPECT_CONTEXT_CHUNK_HEADER_1' let mat = matchlist(line, '^\*\*\* \(\d\+,\)\?\(\d\+\) \*\*\*\*$') if empty(mat) || mat[1] == '' State 'START' let linum -= 1 Debug line . State() continue endif let collect += [line] State 'SKIP_CONTEXT_STUFF_1' Debug line . State() continue elseif State() == 'SKIP_CONTEXT_STUFF_1' if line !~ '^[ !+].*$' let mat = matchlist(line, '^--- \(\d\+\),\(\d\+\) ----$') if ! empty(mat) && mat[1] != '' && mat[2] != '' let goal_count = mat[2] - mat[1] + 1 let c_count = 0 State 'READ_CONTEXT_CHUNK' let collect += [line] Debug line . State() . " Goal count set to " . goal_count continue endif State 'START' let linum -= 1 Debug line . State() continue endif let collect += [line] continue elseif State() == 'READ_CONTEXT_CHUNK' let c_count += 1 if c_count == goal_count let collect += [line] State 'BACKSLASH_OR_CRANGE_EOF' continue else " goal not met yet let mat = matchlist(line, '^\([\\!+ ]\).*$') if empty(mat) || mat[1] == '' let linum -= 1 State 'START' Debug line . State() continue endif let collect += [line] continue endif elseif State() == 'BACKSLASH_OR_CRANGE_EOF' if line =~ '^\\ No newline.*$' " XXX: Can we go to another chunk from here?? let collect += [line] let this_patch = {} let this_patch['filename'] = filepath let this_patch['type'] = p_type let this_patch['content'] = collect let g:patches['patch'] += [this_patch] Debug "Patch collected for " . filepath State 'START' continue endif if line =~ '^\*\{15}$' let collect += [line] State 'EXPECT_CONTEXT_CHUNK_HEADER_1' Debug line . State() continue endif let this_patch = {} let this_patch['filename'] = filepath let this_patch['type'] = p_type let this_patch['content'] = collect let g:patches['patch'] += [this_patch] let linum -= 1 State 'START' Debug "Patch collected for " . filepath Debug line . State() continue elseif State() == 'MAYBE_UNIFIED_DIFF' let mat = matchlist(line, '^+++ \([^\t]\+\).*$') if empty(mat) || mat[1] == '' State 'START' let linum -= 1 Debug line . State() continue endif let p_second_file = mat[1] if p_first_file == '/dev/null' if p_second_file == '/dev/null' let g:patches['reason'] = "Malformed diff found at line " . linum return endif let p_type = '+' let filepath = p_second_file else if p_second_file == '/dev/null' let p_type = '-' let filepath = p_first_file else let p_type = '!' let filepath = p_first_file endif endif State 'EXPECT_UNIFIED_RANGE_CHUNK' let collect += [line] Debug line . State() continue elseif State() == 'EXPECT_UNIFIED_RANGE_CHUNK' let mat = matchlist(line, '^@@ -\(\d\+,\)\?\(\d\+\) +\(\d\+,\)\?\(\d\+\) @@$') if ! empty(mat) let old_goal_count = mat[2] let new_goal_count = mat[4] let o_count = 0 let n_count = 0 Debug "Goal count set to " . old_goal_count . ', ' . new_goal_count State 'READ_UNIFIED_CHUNK' let collect += [line] Debug line . State() continue endif State 'START' Debug line . State() continue elseif State() == 'READ_UNIFIED_CHUNK' if o_count == old_goal_count && n_count == new_goal_count if line =~ '^\\.*$' " XXX: Can we go to another chunk from here?? let collect += [line] let this_patch = {} let this_patch['filename'] = filepath let this_patch['type'] = p_type let this_patch['content'] = collect let g:patches['patch'] += [this_patch] Debug "Patch collected for " . filepath State 'START' continue endif let mat = matchlist(line, '^@@ -\(\d\+,\)\?\(\d\+\) +\(\d\+,\)\?\(\d\+\) @@$') if ! empty(mat) let old_goal_count = mat[2] let new_goal_count = mat[4] let o_count = 0 let n_count = 0 Debug "Goal count set to " . old_goal_count . ', ' . new_goal_count let collect += [line] Debug line . State() continue endif let this_patch = {} let this_patch['filename'] = filepath let this_patch['type'] = p_type let this_patch['content'] = collect let g:patches['patch'] += [this_patch] Debug "Patch collected for " . filepath let linum -= 1 State 'START' Debug line . State() continue else " goal not met yet let mat = matchlist(line, '^\([\\+ -]\).*$') if empty(mat) || mat[1] == '' let linum -= 1 State 'START' continue endif let chr = mat[1] if chr == '+' let n_count += 1 endif if chr == ' ' let o_count += 1 let n_count += 1 endif if chr == '-' let o_count += 1 endif let collect += [line] Debug line . State() continue endif else let g:patches['reason'] = "Internal error: Do not use the plugin anymore and if possible please send the diff or patch file you tried it with to Manpreet Singh " return endif endwhile "Pecho State() if (State() == 'READ_CONTEXT_CHUNK' && c_count == goal_count) || (State() == 'READ_UNIFIED_CHUNK' && n_count == new_goal_count && o_count == old_goal_count) let this_patch = {} let this_patch['filename'] = filepath let this_patch['type'] = p_type let this_patch['content'] = collect let g:patches['patch'] += [this_patch] Debug "Patch collected for " . filepath endif return endfunction "}}} function! State(...) " For easy manipulation of diff extraction state "{{{ if a:0 != 0 let s:STATE = a:1 else if ! exists('s:STATE') let s:STATE = 'START' endif return s:STATE endif endfunction com! -nargs=+ -complete=expression State call State() "}}} function! PatchReview(...) "{{{ let s:save_shortmess = &shortmess let s:save_aw = &autowrite let s:save_awa = &autowriteall set shortmess=aW call s:PR_wipeMsgBuf() let s:reviewmode = 'patch' call s:_GenericReview(a:000) let &autowriteall = s:save_awa let &autowrite = s:save_aw let &shortmess = s:save_shortmess endfunction "}}} function! _GenericReview(argslist) "{{{ " diff mode: " arg1 = patchfile " arg2 = strip count " patch mode: " arg1 = patchfile " arg2 = strip count " arg3 = directory " VIM 7+ required if version < 700 Pecho 'This plugin needs VIM 7 or higher' return endif " +diff required if ! has('diff') Pecho 'This plugin needs VIM built with +diff feature.' return endif if s:reviewmode == 'diff' let patch_R_option = ' -t -R ' elseif s:reviewmode == 'patch' let patch_R_option = '' else Pecho 'Fatal internal error in patchreview.vim plugin' return endif " Check passed arguments if len(a:argslist) == 0 Pecho 'PatchReview command needs at least one argument specifying a patchfile path.' return endif let StripCount = 0 if len(a:argslist) >= 1 && ((s:reviewmode == 'patch' && len(a:argslist) <= 3) || (s:reviewmode == 'diff' && len(a:argslist) == 2)) let PatchFilePath = expand(a:argslist[0], ':p') if ! filereadable(PatchFilePath) Pecho 'File [' . PatchFilePath . '] is not accessible.' return endif if len(a:argslist) >= 2 && s:reviewmode == 'patch' let s:SrcDirectory = expand(a:argslist[1], ':p') if ! isdirectory(s:SrcDirectory) Pecho '[' . s:SrcDirectory . '] is not a directory' return endif try " Command line has already escaped the path exe 'cd ' . s:SrcDirectory catch /^.*E344.*/ Pecho 'Could not change to directory [' . s:SrcDirectory . ']' return endtry endif if s:reviewmode == 'diff' " passed in by default let StripCount = eval(a:argslist[1]) elseif s:reviewmode == 'patch' let StripCount = 1 " optional strip count if len(a:argslist) == 3 let StripCount = eval(a:argslist[2]) endif endif else if s:reviewmode == 'patch' Pecho 'PatchReview command needs at most three arguments: patchfile path, optional source directory path and optional strip count.' elseif s:reviewmode == 'diff' Pecho 'DiffReview command accepts no arguments.' endif return endif " Verify that patch command and temporary directory are available or specified if ! s:PR_checkBinary('patch') return endif " Requirements met, now execute let PatchFilePath = fnamemodify(PatchFilePath, ':p') if s:reviewmode == 'patch' Pecho 'Patch file : ' . PatchFilePath endif Pecho 'Source directory: ' . getcwd() Pecho '------------------' if s:PR_checkBinary('filterdiff') Debug "Using filterdiff" call s:ExtractDiffsNative(PatchFilePath) else Debug "Using own diff extraction (slower)" call s:ExtractDiffsPureVim(PatchFilePath) endif for patch in g:patches['patch'] if patch.type !~ '^[!+-]$' Pecho '*** Skipping review generation due to unknown change [' . patch.type . ']', 1 continue endif unlet! relpath let relpath = patch.filename " XXX: svn diff and hg diff produce different kind of outputs, one requires " XXX: stripping but the other doesn't. We need to take care of that let stripmore = StripCount let StrippedRelativeFilePath = relpath while stripmore > 0 " strip one let StrippedRelativeFilePath = substitute(StrippedRelativeFilePath, '^[^\\\/]\+[^\\\/]*[\\\/]' , '' , '') let stripmore -= 1 endwhile if patch.type == '!' if s:reviewmode == 'patch' let msgtype = 'Patch modifies file: ' elseif s:reviewmode == 'diff' let msgtype = 'File has changes: ' endif elseif patch.type == '+' if s:reviewmode == 'patch' let msgtype = 'Patch adds file : ' elseif s:reviewmode == 'diff' let msgtype = 'New file : ' endif elseif patch.type == '-' if s:reviewmode == 'patch' let msgtype = 'Patch removes file : ' elseif s:reviewmode == 'diff' let msgtype = 'Removed file : ' endif endif let bufnum = bufnr(relpath) if buflisted(bufnum) && getbufvar(bufnum, '&mod') Pecho 'Old buffer for file [' . relpath . '] exists in modified state. Skipping review.', 1 continue endif let tmpname = tempname() " write patch for patch.filename into tmpname call writefile(patch.content, tmpname) if patch.type == '+' && s:reviewmode == 'patch' let inputfile = '' let patchcmd = '!' . g:patchreview_patch . patch_R_option . ' -o "' . tmpname . '.file" "' . inputfile . '" < "' . tmpname . '"' elseif patch.type == '+' && s:reviewmode == 'diff' let inputfile = '' unlet! patchcmd else let inputfile = expand(StrippedRelativeFilePath, ':p') let patchcmd = '!' . g:patchreview_patch . patch_R_option . ' -o "' . tmpname . '.file" "' . inputfile . '" < "' . tmpname . '"' endif if exists('patchcmd') let v:errmsg = '' Debug patchcmd silent exe patchcmd if v:errmsg != '' || v:shell_error Pecho 'ERROR: Could not execute patch command.' Pecho 'ERROR: ' . patchcmd Pecho 'ERROR: ' . v:errmsg Pecho 'ERROR: Diff skipped.' continue endif endif call delete(tmpname) let s:origtabpagenr = tabpagenr() silent! exe 'tabedit ' . StrippedRelativeFilePath if exists('patchcmd') " modelines in loaded files mess with diff comparision let s:keep_modeline=&modeline let &modeline=0 silent! exe 'vert diffsplit ' . tmpname . '.file' setlocal buftype=nofile setlocal noswapfile setlocal syntax=none setlocal bufhidden=delete setlocal nobuflisted setlocal modifiable setlocal nowrap " Remove buffer name silent! 0f " Switch to original to get a nice tab title silent! wincmd p let &modeline=s:keep_modeline else silent! exe 'vnew' endif if filereadable(tmpname . '.file.rej') silent! exe 'topleft 5split ' . tmpname . '.file.rej' Pecho msgtype . '*** REJECTED *** ' . relpath, 1 else Pecho msgtype . ' ' . relpath, 1 endif silent! exe 'tabn ' . s:origtabpagenr endfor Pecho '-----' Pecho 'Done.' endfunction "}}} function! DiffReview(...) "{{{ let s:save_shortmess = &shortmess set shortmess=aW call s:PR_wipeMsgBuf() let vcsdict = { \'Mercurial' : {'dir' : '.hg', 'binary' : 'hg', 'diffargs' : 'diff' , 'strip' : 1}, \'Bazaar-NG' : {'dir' : '.bzr', 'binary' : 'bzr', 'diffargs' : 'diff' , 'strip' : 0}, \'monotone' : {'dir' : '_MTN', 'binary' : 'mtn', 'diffargs' : 'diff --unified', 'strip' : 0}, \'Subversion' : {'dir' : '.svn', 'binary' : 'svn', 'diffargs' : 'diff' , 'strip' : 0}, \'cvs' : {'dir' : 'CVS', 'binary' : 'cvs', 'diffargs' : '-q diff -u' , 'strip' : 0}, \} unlet! s:theDiffCmd unlet! l:vcs if ! exists('g:patchreview_diffcmd') for key in keys(vcsdict) if isdirectory(vcsdict[key]['dir']) if ! s:PR_checkBinary(vcsdict[key]['binary']) Pecho 'Current directory looks like a ' . vcsdict[key] . ' repository but ' . vcsdist[key]['binary'] . ' command was not found on path.' let &shortmess = s:save_shortmess return else let s:theDiffCmd = vcsdict[key]['binary'] . ' ' . vcsdict[key]['diffargs'] let strip = vcsdict[key]['strip'] Pecho 'Using [' . s:theDiffCmd . '] to generate diffs for this ' . key . ' review.' let &shortmess = s:save_shortmess let l:vcs = vcsdict[key]['binary'] break endif else continue endif endfor else let s:theDiffCmd = g:patchreview_diffcmd let strip = 0 endif if ! exists('s:theDiffCmd') Pecho 'Please define g:patchreview_diffcmd and make sure you are in a VCS controlled top directory.' let &shortmess = s:save_shortmess return endif let outfile = tempname() let cmd = s:theDiffCmd . ' > "' . outfile . '"' let v:errmsg = '' let cout = system(cmd) if v:errmsg == '' && exists('l:vcs') && l:vcs == 'cvs' && v:shell_error == 1 " Ignoring CVS non-error elseif v:errmsg != '' || v:shell_error Pecho v:errmsg Pecho 'Could not execute [' . s:theDiffCmd . ']' Pecho 'Error code: ' . v:shell_error Pecho cout Pecho 'Diff review aborted.' let &shortmess = s:save_shortmess return endif let s:reviewmode = 'diff' call s:_GenericReview([outfile, strip]) let &shortmess = s:save_shortmess endfunction "}}} " End user commands "{{{ "============================================================================ " :PatchReview command! -nargs=* -complete=file PatchReview call s:PatchReview () " :DiffReview command! -nargs=0 DiffReview call s:DiffReview() "}}} " Development "{{{ if exists('g:patchreview_debug') " Tests function! PRExtractTestNative(...) "let patchfiles = glob(expand(a:1) . '/?*') "for fname in split(patchfiles) call s:PR_wipeMsgBuf() let fname = a:1 call s:ExtractDiffsNative(fname) for patch in g:patches['patch'] for line in patch.content Pecho line endfor endfor "endfor endfunction function! PRExtractTestVim(...) "let patchfiles = glob(expand(a:1) . '/?*') "for fname in split(patchfiles) call s:PR_wipeMsgBuf() let fname = a:1 call s:ExtractDiffsPureVim(fname) for patch in g:patches['patch'] for line in patch.content Pecho line endfor endfor "endfor endfunction command! -nargs=+ -complete=file PRTestVim call s:PRExtractTestVim() command! -nargs=+ -complete=file PRTestNative call s:PRExtractTestNative() endif "}}} " modeline " vim: set et fdl=0 fdm=marker fenc=latin ff=unix ft=vim sw=2 sts=0 ts=2 textwidth=78 nowrap :