diff --git a/README.md b/README.md index fbe96ba..e7faa7b 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,30 @@ ## Fetch that line and column, boy! -*vim-fetch* enables Vim to process line and column jump specifications in file paths as found in stack traces and similar output. When asked to open such a file, Vim with *vim-fetch* will jump to the specified line (and column, if given) instead of displaying an empty, new file. +*vim-fetch* enables Vim to process line and column jump specifications in file paths as found in stack traces and similar output. When asked to open such a file, in- or outside Vim or via `gF`, Vim with *vim-fetch* will jump to the specified line (and column, if given) instead of displaying an empty, new file. -If you have wished Vim would understand stack trace formats when opening files, *vim-fetch* is for you. +![](img/vim-fetch.gif "vim-fetch edit functionality demo") -### Installation - -1. The old way: download and source the vimball from the [releases page][releases], then run `:helptags {dir}` on your runtimepath/doc directory. Or, -2. The plug-in manager way: using a git-based plug-in manager (Pathogen, Vundle, NeoBundle etc.), simply add `kopischke/vim-fetch` to the list of plug-ins, source that and issue your manager's install command. +If you have wished Vim would have a better understanding of stack trace formats than what it offers out of the box, *vim-fetch* is for you. ### Usage -TL;DR: `vim path/to/file.ext:12:3` in the shell to open `file.ext`on line 12 at column 3, or `:e[dit] path/to/file.ext:100:12` in Vim to edit `file.ext` on line 100 at column 12. For more, see the [documentation][doc]. +- `vim path/to/file.ext:12:3` in the shell to open `file.ext`on line 12 at column 3 +- `:e[dit] path/to/file.ext:100:12` in Vim to edit `file.ext` on line 100 at column 12 +- `gF` with the cursor at `^` on `path/to^/file.ext:98,8` to edit `file.ext` on line 98, column 8 +- `gF` with the selection `|...|` on `|path to/file.ext|:5:2` to edit `file.ext` on line 5, column 2 + +Besides the GNU colon format, *vim-fetch* supports various other jump specification formats, including some that search for keywords or method definitions. For more, see the [documentation][doc]. ### Rationale Quickly jumping to the point indicated by common stack trace output should be a given in an editor; unluckily, Vim has no concept of this out of the box that does not involve a rather convoluted detour through an error file and the Quickfix window. As the one plug-in I found that aims to fix this, Victor Bogado’s [*file_line*][bogado-plugin], had a number of issues (at the time of this writing, it didn’t correctly process multiple files given with a window switch, i.e. [`-o`, `-O`][bogado-issue-winswitch] and [`-p`][bogado-issue-tabswitch], and I found it choked autocommand processing for the first loaded file on the arglist), I wrote my own. +### Installation + +1. The old way: download and source the vimball from the [releases page][releases], then run `:helptags {dir}` on your runtimepath/doc directory. Or, +2. The plug-in manager way: using a git-based plug-in manager (Pathogen, Vundle, NeoBundle etc.), simply add `kopischke/vim-fetch` to the list of plug-ins, source that and issue your manager's install command. + ### License *vim-fetch* is licensed under [the terms of the MIT license according to the accompanying license file][license]. diff --git a/autoload/fetch.vim b/autoload/fetch.vim index 30433fa..d309303 100644 --- a/autoload/fetch.vim +++ b/autoload/fetch.vim @@ -1,143 +1,240 @@ " AUTOLOAD FUNCTION LIBRARY FOR VIM-FETCH -let s:cpo = &cpo -set cpo&vim +if &compatible || v:version < 700 + finish +endif -" Position specs Dictionary: +let s:cpoptions = &cpoptions +set cpoptions&vim + +" Position specs Dictionary: {{{ let s:specs = {} " - trailing colon, i.e. ':lnum[:colnum[:]]' " trigger with '?*:[0123456789]*' pattern -let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?$'} +let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?'} function! s:specs.colon.parse(file) abort - return [substitute(a:file, self.pattern, '', ''), - \ split(matchstr(a:file, self.pattern), ':')] + let l:file = substitute(a:file, self.pattern, '', '') + let l:pos = split(matchstr(a:file, self.pattern), ':') + return [l:file, ['cursor', [l:pos[0], get(l:pos, 1, 0)]]] endfunction " - trailing parentheses, i.e. '(lnum[:colnum])' " trigger with '?*([0123456789]*)' pattern -let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))$'} +let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))'} function! s:specs.paren.parse(file) abort - return [substitute(a:file, self.pattern, '', ''), - \ split(matchlist(a:file, self.pattern)[1], ':')] + let l:file = substitute(a:file, self.pattern, '', '') + let l:pos = split(matchlist(a:file, self.pattern)[1], ':') + return [l:file, ['cursor', [l:pos[0], get(l:pos, 1, 0)]]] endfunction " - Plan 9 type line spec, i.e. '[:]#lnum' " trigger with '?*#[0123456789]*' pattern -let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)$'} +let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)'} function! s:specs.plan9.parse(file) abort - return [substitute(a:file, self.pattern, '', ''), - \ [matchlist(a:file, self.pattern)[1]]] + let l:file = substitute(a:file, self.pattern, '', '') + let l:pos = matchlist(a:file, self.pattern)[1] + return [l:file, ['cursor', [l:pos, 0]]] endfunction -" Detection methods for buffers that bypass `filereadable()`: -let s:ignore = [] - -" - non-file buffer types -call add(s:ignore, {'types': ['quickfix', 'acwrite', 'nofile']}) -function! s:ignore[-1].detect(buffer) abort - return index(self.types, getbufvar(a:buffer, '&buftype')) isnot -1 +" - Pytest type method spec, i.e. ::method +" trigger with '?*::?*' pattern +let s:specs.pytest = {'pattern': '\m::\(\w\+\)'} +function! s:specs.pytest.parse(file) abort + let l:file = substitute(a:file, self.pattern, '', '') + let l:name = matchlist(a:file, self.pattern)[1] + let l:method = '\m\C^\s*def\s\+\%(\\\n\s*\)*\zs'.l:name.'\s*(' + return [l:file, ['search', [l:method, 'cw']]] +endfunction " }}} + +" Detection heuristics for buffers that should not be resolved: {{{ +let s:bufignore = {'freaks': []} +function! s:bufignore.detect(bufnr) abort + for l:freak in self.freaks + if l:freak.detect(a:bufnr) is 1 + return 1 + endif + endfor + return filereadable(bufname(a:bufnr)) endfunction -" - non-document file types that do not trigger the above -" not needed for: Unite / VimFiler / VimShell / CtrlP / Conque-Shell -call add(s:ignore, {'types': ['netrw']}) -function! s:ignore[-1].detect(buffer) abort - return index(self.types, getbufvar(a:buffer, '&filetype')) isnot -1 +" - unlisted status as a catch-all for UI type buffers +call add(s:bufignore.freaks, {}) +function! s:bufignore.freaks[-1].detect(buffer) abort + return buflisted(a:buffer) is 0 endfunction -" - redirected buffers -call add(s:ignore, {'bufvars': ['netrw_lastfile']}) -function! s:ignore[-1].detect(buffer) abort - for l:var in self.bufvars - if !empty(getbufvar(a:buffer, l:var)) - return 1 - endif - endfor - return 0 +" - any 'buftype' but empty and "nowrite" as explicitly marked "not a file" +call add(s:bufignore.freaks, {'buftypes': ['', 'nowrite']}) +function! s:bufignore.freaks[-1].detect(buffer) abort + return index(self.buftypes, getbufvar(a:buffer, '&buftype')) is -1 endfunction +" - out-of-filesystem Netrw file buffers +call add(s:bufignore.freaks, {}) +function! s:bufignore.freaks[-1].detect(buffer) abort + return !empty(getbufvar(a:buffer, 'netrw_lastfile')) +endfunction " }}} + " Get a copy of vim-fetch's spec matchers: " @signature: fetch#specs() " @returns: Dictionary of specs, keyed by name, " each spec Dictionary with the following keys: -" - 'pattern' String to match the spec in a file name -" - 'parse' Funcref taking a spec'ed file name and -" returning a two item List of -" {unspec'ed path:String}, {pos:List} -" @notes: the autocommand match patterns are not included -function! fetch#specs() abort +" -'pattern' String to match the spec in a file name +" -'parse' Funcref taking a spec'ed file name +" and returning a List of +" 0 unspec'ed path String +" 1 position setting |call()| arguments List +" @notes: the autocommand match patterns are not included +function! fetch#specs() abort " {{{ return deepcopy(s:specs) -endfunction +endfunction " }}} + +" Resolve {spec} for the current buffer, substituting the resolved +" file (if any) for it, with the cursor placed at the resolved position: +" @signature: fetch#buffer({spec:String}) +" @returns: Boolean +function! fetch#buffer(spec) abort " {{{ + let l:bufname = expand('%') + let l:spec = s:specs[a:spec] -" Edit {file}, placing the cursor at the line and column indicated by {spec}: -" @signature: fetch#edit({file:String}, {spec:String}) -" @returns: Boolean indicating if a spec has been succesfully resolved -" @notes: - won't work from a |BufReadCmd| event as it doesn't load non-spec'ed files -" - won't work from events fired before the spec'ed file is loaded into -" the buffer (i.e. before '%' is set to the spec'ed file) like |BufNew| -" as it won't be able to wipe the spurious new spec'ed buffer -function! fetch#edit(file, spec) abort - " naive early exit on obvious non-matches - if filereadable(a:file) || match(a:file, s:specs[a:spec].pattern) is -1 + " exclude obvious non-matches + if matchend(l:bufname, l:spec.pattern) isnot len(l:bufname) return 0 endif - " check for unspec'ed editable file - let [l:file, l:pos] = s:specs[a:spec].parse(a:file) - if !filereadable(l:file) - return 0 " in doubt, end with invalid user input + " only substitute if we have a valid resolved file + " and a spurious unresolved buffer both + let [l:file, l:jump] = l:spec.parse(l:bufname) + if !filereadable(l:file) || s:bufignore.detect(bufnr('%')) is 1 + return 0 endif - " processing setup - let l:pre = '' " will be prefixed to edit command + " we have a spurious unresolved buffer: set up for wiping + set buftype=nowrite " avoid issues voiding the buffer + set bufhidden=wipe " avoid issues with |bwipeout| - " if current buffer is spec'ed and invalid set it up for wiping - if expand('%:p') is fnamemodify(a:file, ':p') - for l:ignore in s:ignore - if l:ignore.detect(bufnr('%')) is 1 - return 0 - endif - endfor - set buftype=nowrite " avoid issues voiding the buffer - set bufhidden=wipe " avoid issues with |bwipeout| - let l:pre .= 'keepalt ' " don't mess up alternate file on switch - endif - - " clean up argument list + " substitute resolved file for unresolved buffer on arglist if has('listcmds') - let l:argidx = index(argv(), a:file) - if l:argidx isnot -1 " substitute un-spec'ed file for spec'ed - execute 'argdelete' fnameescape(a:file) + let l:argidx = index(argv(), l:bufname) + if l:argidx isnot -1 + execute 'argdelete' fnameescape(l:bufname) execute l:argidx.'argadd' fnameescape(l:file) endif endif - " edit on argument list if required + " set arglist index to resolved file if required + " (needs to happen independently of arglist switching to work + " with the double processing of the first -o/-O/-p window) if index(argv(), l:file) isnot -1 - let l:pre .= 'arg' " set arglist index to edited file + let l:cmd = 'argedit' endif - " open correct file and place cursor at position spec - execute l:pre.'edit' fnameescape(l:file) - return fetch#setpos(l:pos) -endfunction + " edit resolved file and place cursor at position spec + execute 'keepalt' get(l:, 'cmd', 'edit').v:cmdarg fnameescape(l:file) + if !empty(v:swapcommand) + execute 'normal' v:swapcommand + endif + return s:setpos(l:jump) +endfunction " }}} + +" Edit ||, resolving a possible trailing spec: +" @signature: fetch#cfile({count:Number}) +" @returns: Boolean +" @notes: - will test all available specs for a match +" - will fall back on Vim's |gF| when no spec matches +function! fetch#cfile(count) abort " {{{ + let l:cfile = expand('') + + if !empty(l:cfile) + " locate '' in current line + let l:pattern = '\M'.escape(l:cfile, '\') + let l:position = searchpos(l:pattern, 'bcn', line('.')) + if l:position == [0, 0] + let l:position = searchpos(l:pattern, 'cn', line('.')) + endif + + " test for a trailing spec, accounting for multi-line '' matches + let l:lines = split(l:cfile, "\n") + let l:line = getline(l:position[0] + len(l:lines) - 1) + let l:offset = (len(l:lines) > 1 ? 0 : l:position[1]) + len(l:lines[-1]) - 1 + for l:spec in values(s:specs) + if match(l:line, l:spec.pattern, l:offset) is l:offset + let l:match = matchstr(l:line, l:spec.pattern, l:offset) + " leverage Vim's own |gf| for opening the file + execute 'normal!' a:count.'gf' + return s:setpos(l:spec.parse(l:cfile.l:match)[1]) + endif + endfor + endif + + " fall back to Vim's |gF| + execute 'normal!' a:count.'gF' + return 1 +endfunction " }}} -" Place the current buffer's cursor at {pos}: -" @signature: fetch#setpos({pos:List}) +" Edit the visually selected file, resolving a possible trailing spec: +" @signature: fetch#visual({count:Number}) " @returns: Boolean -" @notes: triggers the |User| events -" - BufFetchPosPre before setting the position -" - BufFetchPosPost after setting the position -function! fetch#setpos(pos) abort - silent doautocmd User BufFetchPosPre - let b:fetch_lastpos = [max([a:pos[0], 1]), max([get(a:pos, 1, 0), 1])] - call cursor(b:fetch_lastpos[0], b:fetch_lastpos[1]) +" @notes: - will test all available specs for a match +" - will fall back on Vim's |gF| when no spec matches +function! fetch#visual(count) abort " {{{ + " get text between last visual selection marks + " adapted from http://stackoverflow.com/a/6271254/990363 + let [l:startline, l:startcol] = getpos("'<")[1:2] + let [l:endline, l:endcol] = getpos("'>")[1:2] + let l:endcol -= &selection is 'inclusive' ? 0 : 1 + let lines = getline(l:startline, l:endline) + let lines[-1] = matchstr(lines[-1], '\m^.\{'.string(l:endcol).'}') + let lines[0] = matchstr(lines[0], '\m^.\{'.string(l:startcol - 1).'}\zs.*') + let l:selection = join(lines, "\n") + + " test for a trailing spec + if !empty(l:selection) + let l:line = getline(l:endline) + for l:spec in values(s:specs) + if match(l:line, l:spec.pattern, l:endcol) is l:endcol + let l:match = matchstr(l:line, l:spec.pattern, l:endcol) + call s:dovisual(a:count.'gf') " leverage Vim's |gf| to get the file + return s:setpos(l:spec.parse(l:selection.l:match)[1]) + endif + endfor + endif + + " fall back to Vim's |gF| + call s:dovisual(a:count.'gF') + return 1 +endfunction " }}} + +" Private helper functions: {{{ +" - place the current buffer's cursor, triggering the "BufFetchPosX" events +" see :h call() for the format of the {calldata} List +function! s:setpos(calldata) abort + call s:doautocmd('BufFetchPosPre') + keepjumps call call('call', a:calldata) + let b:fetch_lastpos = getpos('.')[1:2] silent! normal! zOzz - silent doautocmd User BufFetchPosPost - return getpos('.')[1:2] == b:fetch_lastpos + call s:doautocmd('BufFetchPosPost') + return 1 +endfunction + +" - apply User autocommands matching {pattern}, but only if there are any +" 1. avoids flooding message history with "No matching autocommands" +" 2. avoids re-applying modelines in Vim < 7.3.442, which doesn't honor || +" see https://groups.google.com/forum/#!topic/vim_dev/DidKMDAsppw +function! s:doautocmd(pattern) abort + if exists('#User#'.a:pattern) + execute 'doautocmd User' a:pattern + endif +endfunction + +" - send command to the last visual selection +function! s:dovisual(command) abort + let l:cmd = index(['v', 'V', ''], mode()) is -1 ? 'gv'.a:command : a:command + execute 'normal!' l:cmd endfunction +" }}} -let &cpo = s:cpo -unlet! s:cpo +let &cpoptions = s:cpoptions +unlet! s:cpoptions " vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}: diff --git a/autoload/stay/integrate/fetch.vim b/autoload/stay/integrate/fetch.vim new file mode 100644 index 0000000..78d9a0f --- /dev/null +++ b/autoload/stay/integrate/fetch.vim @@ -0,0 +1,14 @@ +" VIM-STAY INTEGRATION MODULE +" https://github.com/kopischke/vim-stay +let s:cpoptions = &cpoptions +set cpoptions&vim + +" - register integration autocommands +function! stay#integrate#fetch#setup() abort + autocmd User BufFetchPosPost let b:stay_atpos = b:fetch_lastpos +endfunction + +let &cpoptions = s:cpoptions +unlet! s:cpoptions + +" vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}: diff --git a/doc/vim-fetch.txt b/doc/vim-fetch.txt index 83cb9d5..f0a556a 100644 --- a/doc/vim-fetch.txt +++ b/doc/vim-fetch.txt @@ -1,4 +1,4 @@ -*vim-fetch.txt* For Vim version 7.0 or better version 1.3.0 +*vim-fetch.txt* For Vim version 7.0 or better version 2.0.0 VIM REFERENCE for the Fetch plug-in @@ -7,8 +7,8 @@ Jump to lines and columns specified in buffer names *vim-fetch* 1. Introduction |vim-fetch-introduction| -2. Usage |vim-fetch-usage| -3. Position specifications |vim-fetch-specs| +2. Position specifications |vim-fetch-specs| +3. Usage |vim-fetch-usage| 4. Troubleshooting |vim-fetch-troubleshooting| 5. Credits and license |vim-fetch-credits-license| @@ -23,7 +23,49 @@ a file, Vim with vim-fetch will jump to the specified line (and column, if given) instead of displaying an empty, new file. ============================================================================== -2. Usage *vim-fetch-usage* +2. Position specifications *vim-fetch-specs* + +vim-fetch understands the following position specifications ("specs"): + + +COLON SEPARATED + +1. path/to/file.ext:lnum +2. path/to/file.ext:lnum: +3. path/to/file.ext:lnum:colnum +4. path/to/file.ext:lnum:colnum: + + +PARENTHESES ENCLOSED + +5. path/to/file.ext(lnum) +6. path/to/file.ext(lnum:colnum) + + +PLAN 9 STYLE + +7. path/to/file.ext:#lnum + +Note: `#` is the alternate file token and needs to be escaped to be used on +the command line (see |cmdline-special|). + + +PYTEST METHOD JUMPS + +8. path/to/file.ext::method + +Note: this will only find Python method definitions. + + +OTHER SPEC TYPES + +If you would you like to see other specs in vim-fetch, open an issue (or even +better: send a PR) for them at + + https://github.com/kopischke/vim-fetch + +============================================================================== +3. Usage *vim-fetch-usage* FROM OUTSIDE VIM: @@ -47,10 +89,18 @@ This works for any command that opens files with |edit| semantics, including |argedit|, |pedit| and |diffsplit| on local file systems (e.g. not for |netrw| remote editing). +Using |gF| on either || or a visual selection will recognize trailing +vim-fetch specs besides the ones supported by Vim, e.g, with the cursor at the +position indicated by `^` +> + Debug info: path^/to/file.ext(170:5) +< +a normal mode `gF` will open `file.ext` and jump to line 170, column 5. + INTEGRATION: -1. position buffer local variable *b:fetch_lastpos* +1. Position buffer local variable *b:fetch_lastpos* After processing a spec for a buffer, vim-fetch sets a buffer-local variable: > @@ -59,53 +109,13 @@ After processing a spec for a buffer, vim-fetch sets a buffer-local variable: You can use the presence and / or values of that variable to integrate with vim-fetch. -2. autocommand API *BufFetchPosPre* *BufFetchPosPost* +2. Autocommand API *vim-fetch-autocommands* vim-fetch triggers two |User| autocommand events when setting the position in -a buffer: `BufFetchPosPre` before and `BufFetchPosPost` after. - -Notes: - BufFetchPosPost is fired if the position fetch function completes, it - does not guarantee the final position of the cursor. - - The events are triggered with |:silent| to avoid flooding message - history with "No matching autocommands" messages. Use |:unsilent| to - restore normal message processing, i.e. -> - autocmd User BufFetchPosPre unsilent echomsg 'Gotcha!' -< -============================================================================== -3. Position specifications *vim-fetch-specs* - -vim-fetch understands the following position specifications: - - -COLON SEPARATED - -1. path/to/file.ext:lnum -2. path/to/file.ext:lnum: -3. path/to/file.ext:lnum:colnum -4. path/to/file.ext:lnum:colnum: - - -PARENTHESES ENCLOSED - -5. path/to/file.ext(lnum) -6. path/to/file.ext(lnum:colnum) +a buffer: *BufFetchPosPre* before and *BufFetchPosPost* after. - -PLAN 9 STYLE - -7. path/to/file.ext:#lnum - -Note: `#` is the alternate file token and needs to be escaped to be used on -the command line (see |cmdline-special|). - - -OTHER SPEC TYPES - -If you would you like to see other specs in vim-fetch, open an issue (or even -better: send a PR) for them at - - https://github.com/kopischke/vim-fetch +Notes: `BufFetchPosPost` is fired if the position fetch function completes, +it does not guarantee the final position of the cursor. ============================================================================== 4. Troubleshooting *vim-fetch-troubleshooting* @@ -140,6 +150,18 @@ and unspec'ed form. Doing that remotely, even when possible, would add too much latency to file opening. +:DROP'ING A SPEC TO AN OPEN FILE DOES NOT ACTIVATE ITS WINDOW + +Short answer: this is a limitation of the way vim-fetch works. + +Long answer: when you |:drop| a spec'ed file, the |:drop| command looks for +the spec'ed version in your open windows, which it doesn't find. It then +"loads" the spec'ed buffer, i.e. creates a spurious one, which vim-fetch in +turn switches out against the correct one; however, vim-fetch has no way to +detect the buffer was originally created by a |:drop| command, so the switch +happens in place, instead of in the window where the buffer is already open. + + MY PROBLEM ISN'T LISTED HERE You might have found a bug. Please open an issue at diff --git a/img/vim-fetch.gif b/img/vim-fetch.gif new file mode 100644 index 0000000..6d36bea Binary files /dev/null and b/img/vim-fetch.gif differ diff --git a/plugin/fetch.vim b/plugin/fetch.vim index e231d15..529deb6 100644 --- a/plugin/fetch.vim +++ b/plugin/fetch.vim @@ -1,13 +1,13 @@ " SIMPLIFIED TAKE ON BOGADO/FILE-LINE (HOPEFULLY) WITHOUT THE WARTS " Maintainer: Martin Kopischke " License: MIT (see LICENSE.md) -" Version: 1.3.0 +" Version: 2.0.0 if &compatible || !has('autocmd') || v:version < 700 finish endif -let s:cpo = &cpo -set cpo&vim +let s:cpoptions = &cpoptions +set cpoptions&vim " Based on |BufNewFile|, but flanked by |BufWinEnter| to correctly process all " buffers in an |arglist| passed with '-o/-O' resp. '-p' (see @@ -24,18 +24,24 @@ let s:matchers = { \ 'colon': '?*:[0123456789]*', \ 'paren': '?*([0123456789]*)', \ 'plan9': '?*#[0123456789]*', + \ 'pytest': '?*::?*', \ } +" Set up autocommands: augroup fetch autocmd! - for [s:spec, s:pat] in items(s:matchers) - execute 'autocmd BufNewFile,BufWinEnter' s:pat - \ 'nested call fetch#edit(expand(""), "'.s:spec.'")' - unlet! s:spec s:pat + for [s:spec, s:pattern] in items(s:matchers) + execute 'autocmd BufNewFile,BufWinEnter' s:pattern + \ 'nested call fetch#buffer("'.s:spec.'")' + unlet! s:spec s:pattern endfor augroup END -let &cpo = s:cpo -unlet! s:cpo +" Set up mappings: +nnoremap gF :call fetch#cfile(v:count1) +xnoremap gF :call fetch#visual(v:count1) + +let &cpoptions = s:cpoptions +unlet! s:cpoptions " vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}: