Skip to content

Commit 640388c

Browse files
committed
Merge branch 'release-2.0.0'
Closes #3.
2 parents e0089cd + 14fe954 commit 640388c

File tree

6 files changed

+302
-156
lines changed

6 files changed

+302
-156
lines changed

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,30 @@
55

66
## Fetch that line and column, boy!
77

8-
*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.
8+
*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.
99

10-
If you have wished Vim would understand stack trace formats when opening files, *vim-fetch* is for you.
10+
![](img/vim-fetch.gif "vim-fetch edit functionality demo")
1111

12-
### Installation
13-
14-
1. The old way: download and source the vimball from the [releases page][releases], then run `:helptags {dir}` on your runtimepath/doc directory. Or,
15-
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.
12+
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.
1613

1714
### Usage
1815

19-
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].
16+
- `vim path/to/file.ext:12:3` in the shell to open `file.ext`on line 12 at column 3
17+
- `:e[dit] path/to/file.ext:100:12` in Vim to edit `file.ext` on line 100 at column 12
18+
- `gF` with the cursor at `^` on `path/to^/file.ext:98,8` to edit `file.ext` on line 98, column 8
19+
- `gF` with the selection `|...|` on `|path to/file.ext|:5:2` to edit `file.ext` on line 5, column 2
20+
21+
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].
2022

2123
### Rationale
2224

2325
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.
2426

27+
### Installation
28+
29+
1. The old way: download and source the vimball from the [releases page][releases], then run `:helptags {dir}` on your runtimepath/doc directory. Or,
30+
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.
31+
2532
### License
2633

2734
*vim-fetch* is licensed under [the terms of the MIT license according to the accompanying license file][license].

autoload/fetch.vim

Lines changed: 188 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,240 @@
11
" AUTOLOAD FUNCTION LIBRARY FOR VIM-FETCH
2-
let s:cpo = &cpo
3-
set cpo&vim
2+
if &compatible || v:version < 700
3+
finish
4+
endif
45

5-
" Position specs Dictionary:
6+
let s:cpoptions = &cpoptions
7+
set cpoptions&vim
8+
9+
" Position specs Dictionary: {{{
610
let s:specs = {}
711

812
" - trailing colon, i.e. ':lnum[:colnum[:]]'
913
" trigger with '?*:[0123456789]*' pattern
10-
let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?$'}
14+
let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?'}
1115
function! s:specs.colon.parse(file) abort
12-
return [substitute(a:file, self.pattern, '', ''),
13-
\ split(matchstr(a:file, self.pattern), ':')]
16+
let l:file = substitute(a:file, self.pattern, '', '')
17+
let l:pos = split(matchstr(a:file, self.pattern), ':')
18+
return [l:file, ['cursor', [l:pos[0], get(l:pos, 1, 0)]]]
1419
endfunction
1520

1621
" - trailing parentheses, i.e. '(lnum[:colnum])'
1722
" trigger with '?*([0123456789]*)' pattern
18-
let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))$'}
23+
let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))'}
1924
function! s:specs.paren.parse(file) abort
20-
return [substitute(a:file, self.pattern, '', ''),
21-
\ split(matchlist(a:file, self.pattern)[1], ':')]
25+
let l:file = substitute(a:file, self.pattern, '', '')
26+
let l:pos = split(matchlist(a:file, self.pattern)[1], ':')
27+
return [l:file, ['cursor', [l:pos[0], get(l:pos, 1, 0)]]]
2228
endfunction
2329

2430
" - Plan 9 type line spec, i.e. '[:]#lnum'
2531
" trigger with '?*#[0123456789]*' pattern
26-
let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)$'}
32+
let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)'}
2733
function! s:specs.plan9.parse(file) abort
28-
return [substitute(a:file, self.pattern, '', ''),
29-
\ [matchlist(a:file, self.pattern)[1]]]
34+
let l:file = substitute(a:file, self.pattern, '', '')
35+
let l:pos = matchlist(a:file, self.pattern)[1]
36+
return [l:file, ['cursor', [l:pos, 0]]]
3037
endfunction
3138

32-
" Detection methods for buffers that bypass `filereadable()`:
33-
let s:ignore = []
34-
35-
" - non-file buffer types
36-
call add(s:ignore, {'types': ['quickfix', 'acwrite', 'nofile']})
37-
function! s:ignore[-1].detect(buffer) abort
38-
return index(self.types, getbufvar(a:buffer, '&buftype')) isnot -1
39+
" - Pytest type method spec, i.e. ::method
40+
" trigger with '?*::?*' pattern
41+
let s:specs.pytest = {'pattern': '\m::\(\w\+\)'}
42+
function! s:specs.pytest.parse(file) abort
43+
let l:file = substitute(a:file, self.pattern, '', '')
44+
let l:name = matchlist(a:file, self.pattern)[1]
45+
let l:method = '\m\C^\s*def\s\+\%(\\\n\s*\)*\zs'.l:name.'\s*('
46+
return [l:file, ['search', [l:method, 'cw']]]
47+
endfunction " }}}
48+
49+
" Detection heuristics for buffers that should not be resolved: {{{
50+
let s:bufignore = {'freaks': []}
51+
function! s:bufignore.detect(bufnr) abort
52+
for l:freak in self.freaks
53+
if l:freak.detect(a:bufnr) is 1
54+
return 1
55+
endif
56+
endfor
57+
return filereadable(bufname(a:bufnr))
3958
endfunction
4059

41-
" - non-document file types that do not trigger the above
42-
" not needed for: Unite / VimFiler / VimShell / CtrlP / Conque-Shell
43-
call add(s:ignore, {'types': ['netrw']})
44-
function! s:ignore[-1].detect(buffer) abort
45-
return index(self.types, getbufvar(a:buffer, '&filetype')) isnot -1
60+
" - unlisted status as a catch-all for UI type buffers
61+
call add(s:bufignore.freaks, {})
62+
function! s:bufignore.freaks[-1].detect(buffer) abort
63+
return buflisted(a:buffer) is 0
4664
endfunction
4765

48-
" - redirected buffers
49-
call add(s:ignore, {'bufvars': ['netrw_lastfile']})
50-
function! s:ignore[-1].detect(buffer) abort
51-
for l:var in self.bufvars
52-
if !empty(getbufvar(a:buffer, l:var))
53-
return 1
54-
endif
55-
endfor
56-
return 0
66+
" - any 'buftype' but empty and "nowrite" as explicitly marked "not a file"
67+
call add(s:bufignore.freaks, {'buftypes': ['', 'nowrite']})
68+
function! s:bufignore.freaks[-1].detect(buffer) abort
69+
return index(self.buftypes, getbufvar(a:buffer, '&buftype')) is -1
5770
endfunction
5871

72+
" - out-of-filesystem Netrw file buffers
73+
call add(s:bufignore.freaks, {})
74+
function! s:bufignore.freaks[-1].detect(buffer) abort
75+
return !empty(getbufvar(a:buffer, 'netrw_lastfile'))
76+
endfunction " }}}
77+
5978
" Get a copy of vim-fetch's spec matchers:
6079
" @signature: fetch#specs()
6180
" @returns: Dictionary<Dictionary> of specs, keyed by name,
6281
" each spec Dictionary with the following keys:
63-
" - 'pattern' String to match the spec in a file name
64-
" - 'parse' Funcref taking a spec'ed file name and
65-
" returning a two item List of
66-
" {unspec'ed path:String}, {pos:List<Number[,Number]>}
67-
" @notes: the autocommand match patterns are not included
68-
function! fetch#specs() abort
82+
" -'pattern' String to match the spec in a file name
83+
" -'parse' Funcref taking a spec'ed file name
84+
" and returning a List of
85+
" 0 unspec'ed path String
86+
" 1 position setting |call()| arguments List
87+
" @notes: the autocommand match patterns are not included
88+
function! fetch#specs() abort " {{{
6989
return deepcopy(s:specs)
70-
endfunction
90+
endfunction " }}}
91+
92+
" Resolve {spec} for the current buffer, substituting the resolved
93+
" file (if any) for it, with the cursor placed at the resolved position:
94+
" @signature: fetch#buffer({spec:String})
95+
" @returns: Boolean
96+
function! fetch#buffer(spec) abort " {{{
97+
let l:bufname = expand('%')
98+
let l:spec = s:specs[a:spec]
7199

72-
" Edit {file}, placing the cursor at the line and column indicated by {spec}:
73-
" @signature: fetch#edit({file:String}, {spec:String})
74-
" @returns: Boolean indicating if a spec has been succesfully resolved
75-
" @notes: - won't work from a |BufReadCmd| event as it doesn't load non-spec'ed files
76-
" - won't work from events fired before the spec'ed file is loaded into
77-
" the buffer (i.e. before '%' is set to the spec'ed file) like |BufNew|
78-
" as it won't be able to wipe the spurious new spec'ed buffer
79-
function! fetch#edit(file, spec) abort
80-
" naive early exit on obvious non-matches
81-
if filereadable(a:file) || match(a:file, s:specs[a:spec].pattern) is -1
100+
" exclude obvious non-matches
101+
if matchend(l:bufname, l:spec.pattern) isnot len(l:bufname)
82102
return 0
83103
endif
84104

85-
" check for unspec'ed editable file
86-
let [l:file, l:pos] = s:specs[a:spec].parse(a:file)
87-
if !filereadable(l:file)
88-
return 0 " in doubt, end with invalid user input
105+
" only substitute if we have a valid resolved file
106+
" and a spurious unresolved buffer both
107+
let [l:file, l:jump] = l:spec.parse(l:bufname)
108+
if !filereadable(l:file) || s:bufignore.detect(bufnr('%')) is 1
109+
return 0
89110
endif
90111

91-
" processing setup
92-
let l:pre = '' " will be prefixed to edit command
112+
" we have a spurious unresolved buffer: set up for wiping
113+
set buftype=nowrite " avoid issues voiding the buffer
114+
set bufhidden=wipe " avoid issues with |bwipeout|
93115

94-
" if current buffer is spec'ed and invalid set it up for wiping
95-
if expand('%:p') is fnamemodify(a:file, ':p')
96-
for l:ignore in s:ignore
97-
if l:ignore.detect(bufnr('%')) is 1
98-
return 0
99-
endif
100-
endfor
101-
set buftype=nowrite " avoid issues voiding the buffer
102-
set bufhidden=wipe " avoid issues with |bwipeout|
103-
let l:pre .= 'keepalt ' " don't mess up alternate file on switch
104-
endif
105-
106-
" clean up argument list
116+
" substitute resolved file for unresolved buffer on arglist
107117
if has('listcmds')
108-
let l:argidx = index(argv(), a:file)
109-
if l:argidx isnot -1 " substitute un-spec'ed file for spec'ed
110-
execute 'argdelete' fnameescape(a:file)
118+
let l:argidx = index(argv(), l:bufname)
119+
if l:argidx isnot -1
120+
execute 'argdelete' fnameescape(l:bufname)
111121
execute l:argidx.'argadd' fnameescape(l:file)
112122
endif
113123
endif
114124

115-
" edit on argument list if required
125+
" set arglist index to resolved file if required
126+
" (needs to happen independently of arglist switching to work
127+
" with the double processing of the first -o/-O/-p window)
116128
if index(argv(), l:file) isnot -1
117-
let l:pre .= 'arg' " set arglist index to edited file
129+
let l:cmd = 'argedit'
118130
endif
119131

120-
" open correct file and place cursor at position spec
121-
execute l:pre.'edit' fnameescape(l:file)
122-
return fetch#setpos(l:pos)
123-
endfunction
132+
" edit resolved file and place cursor at position spec
133+
execute 'keepalt' get(l:, 'cmd', 'edit').v:cmdarg fnameescape(l:file)
134+
if !empty(v:swapcommand)
135+
execute 'normal' v:swapcommand
136+
endif
137+
return s:setpos(l:jump)
138+
endfunction " }}}
139+
140+
" Edit |<cfile>|, resolving a possible trailing spec:
141+
" @signature: fetch#cfile({count:Number})
142+
" @returns: Boolean
143+
" @notes: - will test all available specs for a match
144+
" - will fall back on Vim's |gF| when no spec matches
145+
function! fetch#cfile(count) abort " {{{
146+
let l:cfile = expand('<cfile>')
147+
148+
if !empty(l:cfile)
149+
" locate '<cfile>' in current line
150+
let l:pattern = '\M'.escape(l:cfile, '\')
151+
let l:position = searchpos(l:pattern, 'bcn', line('.'))
152+
if l:position == [0, 0]
153+
let l:position = searchpos(l:pattern, 'cn', line('.'))
154+
endif
155+
156+
" test for a trailing spec, accounting for multi-line '<cfile>' matches
157+
let l:lines = split(l:cfile, "\n")
158+
let l:line = getline(l:position[0] + len(l:lines) - 1)
159+
let l:offset = (len(l:lines) > 1 ? 0 : l:position[1]) + len(l:lines[-1]) - 1
160+
for l:spec in values(s:specs)
161+
if match(l:line, l:spec.pattern, l:offset) is l:offset
162+
let l:match = matchstr(l:line, l:spec.pattern, l:offset)
163+
" leverage Vim's own |gf| for opening the file
164+
execute 'normal!' a:count.'gf'
165+
return s:setpos(l:spec.parse(l:cfile.l:match)[1])
166+
endif
167+
endfor
168+
endif
169+
170+
" fall back to Vim's |gF|
171+
execute 'normal!' a:count.'gF'
172+
return 1
173+
endfunction " }}}
124174

125-
" Place the current buffer's cursor at {pos}:
126-
" @signature: fetch#setpos({pos:List<Number[,Number]>})
175+
" Edit the visually selected file, resolving a possible trailing spec:
176+
" @signature: fetch#visual({count:Number})
127177
" @returns: Boolean
128-
" @notes: triggers the |User| events
129-
" - BufFetchPosPre before setting the position
130-
" - BufFetchPosPost after setting the position
131-
function! fetch#setpos(pos) abort
132-
silent doautocmd <nomodeline> User BufFetchPosPre
133-
let b:fetch_lastpos = [max([a:pos[0], 1]), max([get(a:pos, 1, 0), 1])]
134-
call cursor(b:fetch_lastpos[0], b:fetch_lastpos[1])
178+
" @notes: - will test all available specs for a match
179+
" - will fall back on Vim's |gF| when no spec matches
180+
function! fetch#visual(count) abort " {{{
181+
" get text between last visual selection marks
182+
" adapted from http://stackoverflow.com/a/6271254/990363
183+
let [l:startline, l:startcol] = getpos("'<")[1:2]
184+
let [l:endline, l:endcol] = getpos("'>")[1:2]
185+
let l:endcol -= &selection is 'inclusive' ? 0 : 1
186+
let lines = getline(l:startline, l:endline)
187+
let lines[-1] = matchstr(lines[-1], '\m^.\{'.string(l:endcol).'}')
188+
let lines[0] = matchstr(lines[0], '\m^.\{'.string(l:startcol - 1).'}\zs.*')
189+
let l:selection = join(lines, "\n")
190+
191+
" test for a trailing spec
192+
if !empty(l:selection)
193+
let l:line = getline(l:endline)
194+
for l:spec in values(s:specs)
195+
if match(l:line, l:spec.pattern, l:endcol) is l:endcol
196+
let l:match = matchstr(l:line, l:spec.pattern, l:endcol)
197+
call s:dovisual(a:count.'gf') " leverage Vim's |gf| to get the file
198+
return s:setpos(l:spec.parse(l:selection.l:match)[1])
199+
endif
200+
endfor
201+
endif
202+
203+
" fall back to Vim's |gF|
204+
call s:dovisual(a:count.'gF')
205+
return 1
206+
endfunction " }}}
207+
208+
" Private helper functions: {{{
209+
" - place the current buffer's cursor, triggering the "BufFetchPosX" events
210+
" see :h call() for the format of the {calldata} List
211+
function! s:setpos(calldata) abort
212+
call s:doautocmd('BufFetchPosPre')
213+
keepjumps call call('call', a:calldata)
214+
let b:fetch_lastpos = getpos('.')[1:2]
135215
silent! normal! zOzz
136-
silent doautocmd <nomodeline> User BufFetchPosPost
137-
return getpos('.')[1:2] == b:fetch_lastpos
216+
call s:doautocmd('BufFetchPosPost')
217+
return 1
218+
endfunction
219+
220+
" - apply User autocommands matching {pattern}, but only if there are any
221+
" 1. avoids flooding message history with "No matching autocommands"
222+
" 2. avoids re-applying modelines in Vim < 7.3.442, which doesn't honor |<nomodeline>|
223+
" see https://groups.google.com/forum/#!topic/vim_dev/DidKMDAsppw
224+
function! s:doautocmd(pattern) abort
225+
if exists('#User#'.a:pattern)
226+
execute 'doautocmd <nomodeline> User' a:pattern
227+
endif
228+
endfunction
229+
230+
" - send command to the last visual selection
231+
function! s:dovisual(command) abort
232+
let l:cmd = index(['v', 'V', ''], mode()) is -1 ? 'gv'.a:command : a:command
233+
execute 'normal!' l:cmd
138234
endfunction
235+
" }}}
139236

140-
let &cpo = s:cpo
141-
unlet! s:cpo
237+
let &cpoptions = s:cpoptions
238+
unlet! s:cpoptions
142239

143240
" vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}:

autoload/stay/integrate/fetch.vim

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
" VIM-STAY INTEGRATION MODULE
2+
" https://github.com/kopischke/vim-stay
3+
let s:cpoptions = &cpoptions
4+
set cpoptions&vim
5+
6+
" - register integration autocommands
7+
function! stay#integrate#fetch#setup() abort
8+
autocmd User BufFetchPosPost let b:stay_atpos = b:fetch_lastpos
9+
endfunction
10+
11+
let &cpoptions = s:cpoptions
12+
unlet! s:cpoptions
13+
14+
" vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}:

0 commit comments

Comments
 (0)