Skip to content

Commit

Permalink
Merge branch 'release-2.0.0'
Browse files Browse the repository at this point in the history
Closes #3.
  • Loading branch information
kopischke committed Feb 25, 2015
2 parents e0089cd + 14fe954 commit 640388c
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 156 deletions.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
279 changes: 188 additions & 91 deletions autoload/fetch.vim
Original file line number Diff line number Diff line change
@@ -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<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<Number[,Number]>}
" @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 |<cfile>|, 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('<cfile>')

if !empty(l:cfile)
" locate '<cfile>' 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 '<cfile>' 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<Number[,Number]>})
" 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 <nomodeline> 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 <nomodeline> 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 |<nomodeline>|
" see https://groups.google.com/forum/#!topic/vim_dev/DidKMDAsppw
function! s:doautocmd(pattern) abort
if exists('#User#'.a:pattern)
execute 'doautocmd <nomodeline> 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={{{,}}}:
14 changes: 14 additions & 0 deletions autoload/stay/integrate/fetch.vim
Original file line number Diff line number Diff line change
@@ -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={{{,}}}:
Loading

0 comments on commit 640388c

Please sign in to comment.