|
1 | 1 | " 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 |
4 | 5 |
|
5 | | -" Position specs Dictionary: |
| 6 | +let s:cpoptions = &cpoptions |
| 7 | +set cpoptions&vim |
| 8 | + |
| 9 | +" Position specs Dictionary: {{{ |
6 | 10 | let s:specs = {} |
7 | 11 |
|
8 | 12 | " - trailing colon, i.e. ':lnum[:colnum[:]]' |
9 | 13 | " trigger with '?*:[0123456789]*' pattern |
10 | | -let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?$'} |
| 14 | +let s:specs.colon = {'pattern': '\m\%(:\d\+\)\{1,2}:\?'} |
11 | 15 | 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)]]] |
14 | 19 | endfunction |
15 | 20 |
|
16 | 21 | " - trailing parentheses, i.e. '(lnum[:colnum])' |
17 | 22 | " trigger with '?*([0123456789]*)' pattern |
18 | | -let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))$'} |
| 23 | +let s:specs.paren = {'pattern': '\m(\(\d\+\%(:\d\+\)\?\))'} |
19 | 24 | 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)]]] |
22 | 28 | endfunction |
23 | 29 |
|
24 | 30 | " - Plan 9 type line spec, i.e. '[:]#lnum' |
25 | 31 | " trigger with '?*#[0123456789]*' pattern |
26 | | -let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)$'} |
| 32 | +let s:specs.plan9 = {'pattern': '\m:#\(\d\+\)'} |
27 | 33 | 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]]] |
30 | 37 | endfunction |
31 | 38 |
|
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)) |
39 | 58 | endfunction |
40 | 59 |
|
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 |
46 | 64 | endfunction |
47 | 65 |
|
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 |
57 | 70 | endfunction |
58 | 71 |
|
| 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 | + |
59 | 78 | " Get a copy of vim-fetch's spec matchers: |
60 | 79 | " @signature: fetch#specs() |
61 | 80 | " @returns: Dictionary<Dictionary> of specs, keyed by name, |
62 | 81 | " 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 " {{{ |
69 | 89 | 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] |
71 | 99 |
|
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) |
82 | 102 | return 0 |
83 | 103 | endif |
84 | 104 |
|
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 |
89 | 110 | endif |
90 | 111 |
|
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| |
93 | 115 |
|
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 |
107 | 117 | 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) |
111 | 121 | execute l:argidx.'argadd' fnameescape(l:file) |
112 | 122 | endif |
113 | 123 | endif |
114 | 124 |
|
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) |
116 | 128 | if index(argv(), l:file) isnot -1 |
117 | | - let l:pre .= 'arg' " set arglist index to edited file |
| 129 | + let l:cmd = 'argedit' |
118 | 130 | endif |
119 | 131 |
|
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 " }}} |
124 | 174 |
|
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}) |
127 | 177 | " @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] |
135 | 215 | 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 |
138 | 234 | endfunction |
| 235 | +" }}} |
139 | 236 |
|
140 | | -let &cpo = s:cpo |
141 | | -unlet! s:cpo |
| 237 | +let &cpoptions = s:cpoptions |
| 238 | +unlet! s:cpoptions |
142 | 239 |
|
143 | 240 | " vim:set sw=2 sts=2 ts=2 et fdm=marker fmr={{{,}}}: |
0 commit comments