forked from vim-scripts/python_match.vim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpython_match.vim
More file actions
302 lines (280 loc) · 10.8 KB
/
Copy pathpython_match.vim
File metadata and controls
302 lines (280 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
" The following only works for jumping between the first matching pair:
"
" let b:match_words = get(b:, 'match_words', &l:matchpairs) . ','
" \ . '^\(\s*\)\zsdef\>' . ':' . '^\1\%('.repeat(' ',shiftwidth()).'\)\+\%(return\|yield\)\>' . ','
" let b:match_words .= ','
" \ . '^\(\s*\)\zs\%(while\|for\)\>' . ':' . '^\1\%('.repeat(' ',shiftwidth()).'\)\+\%(break\|continue\)\>' . ','
"
" instead crudely add support for def and return/yield to the python matchit
" plug-in below (crudely, because return/yield now also match while and for):
" Python filetype plugin for matching with % key
"
" Last Change: 2024-03-11 (add support for def and return/yield)
" Penultimate Change: Thu 02 Oct 2003 12:12:20 PM EDT
" Language: Python (ft=python)
" (Former?!) Maintainer: Benji Fisher, Ph.D. <[email protected]>
" Version: 0.5, for Vim 6.1
" URL: http://www.vim.org/scripts/script.php?script_id=386
"
" Prevent duplicate loading
"
if exists("b:loaded_python_matchit") | finish | endif
let b:loaded_python_matchit = 1
let s:save_cpo = &cpo
set cpo&vim
" % for if -> elif -> else -> if, g% for else -> elif -> if -> else
nnoremap <buffer> <silent> % :<C-U>call <SID>PyMatch('%','n') <CR>
xnoremap <buffer> <silent> % :<C-U>call <SID>PyMatch('%','v') <CR>m'gv``
onoremap <buffer> <silent> % v:<C-U>call <SID>PyMatch('%','o') <CR>
nnoremap <buffer> <silent> g% :<C-U>call <SID>PyMatch('g%','n') <CR>
xnoremap <buffer> <silent> g% :<C-U>call <SID>PyMatch('g%','v') <CR>m'gv``
onoremap <buffer> <silent> g% v:<C-U>call <SID>PyMatch('g%','o') <CR>
" Move to the start ([%) or end (]%) of the current block.
nnoremap <buffer> <silent> [% :<C-U>call <SID>PyMatch('[%', 'n') <CR>
xnoremap <buffer> <silent> [% :<C-U>call <SID>PyMatch('[%','v') <CR>m'gv``
onoremap <buffer> <silent> [% v:<C-U>call <SID>PyMatch('[%', 'o') <CR>
nnoremap <buffer> <silent> ]% :<C-U>call <SID>PyMatch(']%', 'n') <CR>
xnoremap <buffer> <silent> ]% :<C-U>call <SID>PyMatch(']%','v') <CR>m'gv``
onoremap <buffer> <silent> ]% v:<C-U>call <SID>PyMatch(']%', 'o') <CR>
" The rest of the file needs to be :sourced only once per session.
if exists('s:loaded_functions') || &cp
finish
endif
let s:loaded_functions = 1
" One problem with matching in Python is that so many parts are optional.
" I deal with this by matching on any known key words at the start of the
" line, if they have the same indent.
"
" Recognize try, except, finally and if, elif, else .
" keywords that start a block:
let s:ini1 = 'try\|if'
" These are special, because the matching words may not have the same indent:
let s:ini2 = 'for\|while'
let s:ini2 .= '\|def'
" keywords that continue or end a block:
let s:tail1 = 'except\|finally'
let s:tail1 .= '\|elif\|else'
" These go with s:ini2 :
let s:tail2 = 'break\|continue'
let s:tail2 .= '\|return\|yield'
" all keywords:
let s:all1 = s:ini1 . '\|' . s:tail1
let s:all2 = s:ini2 . '\|' . s:tail2
fun! s:PyMatch(type, mode) range
" I have to do this before the :normal gv...
let cnt = v:count1
" If this function was called from Visual mode, make sure that the cursor
" is at the correct end of the Visual range:
if a:mode == "v"
execute "normal! gv\<Esc>"
endif
" Use default behavior if called as % with a count.
if a:type == "%" && v:count
exe "normal! " . v:count . "%"
return s:CleanUp('', a:mode)
endif
" Do not change these: needed for s:CleanUp()
let s:startline = line(".")
let s:startcol = col(".")
" In case we start on a comment line, ...
if a:type == '[%' || a:type == ']%'
let currline = s:NonComment(+1, s:startline-1)
else
let currline = s:startline
endif
let startindent = indent(currline)
" Set a mark before jumping.
normal! m'
" If called as [%, find the start of the current block.
" If called as ]%, find the end of the current block.
if a:type == '[%' || a:type == ']%'
while cnt > 0
let currline = (a:type == '[%') ?
\ s:StartOfBlock(currline) : s:EndOfBlock(currline)
let cnt = cnt - 1
endwhile
execute currline
return s:CleanUp('', a:mode, '$')
endif
" If called as % or g%, decide whether to bail out.
if a:type == '%' || a:type == 'g%'
let text = getline(currline)
if strpart(text, 0, col(".")) =~ '\S\s'
\ || text !~ '^\s*\%(' . s:all1 . '\|' . s:all2 . '\)'
" cursor not on the first WORD or no keyword so bail out
if a:type == '%'
normal! %
endif
return s:CleanUp('', a:mode)
endif
" If it matches s:all2, we need to find the "for" or "while".
if text =~ '^\s*\%(' . s:all2 . '\)'
let topline = currline
while getline(topline) !~ '^\s*\%(' . s:ini2 . '\)'
let temp = s:StartOfBlock(topline)
if temp == topline " there is no enclosing block.
return s:CleanUp('', a:mode)
endif
let topline = temp
endwhile
let topindent = indent(topline)
endif
endif
" If called as %, look down for "elif" or "else" or up for "if".
if a:type == '%' && text =~ '^\s*\%('. s:all1 .'\)'
let next = s:NonComment(+1, currline)
while next > 0 && indent(next) > startindent
let next = s:NonComment(+1, next)
endwhile
if next == 0 || indent(next) < startindent
\ || getline(next) !~ '^\s*\%(' . s:tail1 . '\)'
" There are no "tail1" keywords below startline in this block. Go to
" the start of the block.
let next = (text =~ '^\s*\%(' . s:ini1 . '\)') ?
\ currline : s:StartOfBlock(currline)
endif
execute next
return s:CleanUp('', a:mode, '$')
endif
" If called as %, look down for "break" or "continue" or up for
" "for" or "while".
if a:type == '%' && text =~ '^\s*\%(' . s:all2 . '\)'
let next = s:NonComment(+1, currline)
while next > 0 && indent(next) > topindent
\ && getline(next) !~ '^\s*\%(' . s:tail2 . '\)'
" Skip over nested "for" or "while" blocks:
if getline(next) =~ '^\s*\%(' . s:ini2 . '\)'
let next = s:EndOfBlock(next)
endif
let next = s:NonComment(+1, next)
endwhile
if indent(next) > topindent && getline(next) =~ '^\s*\%(' . s:tail2 . '\)'
execute next
else " There are no "tail2" keywords below v:startline, so go to topline.
execute topline
endif
return s:CleanUp('', a:mode, '$')
endif
" If called as g%, look up for "if" or "elif" or "else" or down for any.
if a:type == 'g%' && text =~ '^\s*\%('. s:all1 .'\)'
" If we started at the top of the block, go down to the end of the block.
if text =~ '^\s*\(' . s:ini1 . '\)'
let next = s:EndOfBlock(currline)
else
let next = s:NonComment(-1, currline)
endif
while next > 0 && indent(next) > startindent
let next = s:NonComment(-1, next)
endwhile
if indent(next) == startindent && getline(next) =~ '^\s*\%('.s:all1.'\)'
execute next
endif
return s:CleanUp('', a:mode, '$')
endif
" If called as g%, look up for "for" or "while" or down for any.
if a:type == 'g%' && text =~ '^\s*\%(' . s:all2 . '\)'
" Start at topline . If we started on a "for" or "while" then topline is
" the same as currline, and we want the last "break" or "continue" in the
" block. Otherwise, we want the last one before currline.
let botline = (topline == currline) ? line("$") + 1 : currline
let currline = topline
let next = s:NonComment(+1, currline)
while next < botline && indent(next) > topindent
if getline(next) =~ '^\s*\%(' . s:tail2 . '\)'
let currline = next
elseif getline(next) =~ '^\s*\%(' . s:ini2 . '\)'
" Skip over nested "for" or "while" blocks:
let next = s:EndOfBlock(next)
endif
let next = s:NonComment(+1, next)
endwhile
execute currline
return s:CleanUp('', a:mode, '$')
endif
endfun
" Return the line number of the next non-comment, or 0 if there is none.
" Start at the current line unless the optional second argument is given.
" The direction is specified by a:inc (normally +1 or -1 ;
" no test for a:inc == 0, which may lead to an infinite loop).
fun! s:NonComment(inc, ...)
if a:0 > 0
let next = a:1 + a:inc
else
let next = line(".") + a:inc
endif
while 0 < next && next <= line("$")
if getline(next) !~ '^\s*\(#\|$\)'
return next
endif
let next = next + a:inc
endwhile
return 0 " If the while loop finishes, we fell off the end of the file.
endfun
" Return the line number of the top of the block containing Line a:start .
" For most lines, this is the first previous line with smaller indent.
" For lines starting with "except", "finally", "elif", or "else", this is the
" first previous line starting with "try" or "if".
fun! s:StartOfBlock(start)
let startindent = indent(a:start)
let tailflag = (getline(a:start) =~ '^\s*\(' . s:tail1 . '\)')
let prevline = s:NonComment(-1, a:start)
while prevline > 0
if indent(prevline) < startindent ||
\ tailflag && indent(prevline) == startindent &&
\ getline(prevline) =~ '^\s*\(' . s:ini1 . '\)'
" Found the start of block!
return prevline
endif
let prevline = s:NonComment(-1, prevline)
endwhile
" If the loop completes, then s:NonComment() returned 0, so we are at the
" top.
return a:start
endfun
" Return the line number of the end of the block containing Line a:start .
" For most lines, this is the line before the next line with smaller indent.
" For lines that begin a block, go to the end of that block, with special
" treatment for "if" and "try" blocks.
fun! s:EndOfBlock(start)
let startindent = indent(a:start)
let currline = a:start
let nextline = s:NonComment(+1, currline)
let startofblock = (indent(nextline) > startindent) ||
\ getline(currline) =~ '^\s*\(' . s:ini1 . '\)'
while nextline > 0
if indent(nextline) < startindent ||
\ startofblock && indent(nextline) == startindent &&
\ getline(nextline) !~ '^\s*\(' . s:tail1 . '\)'
break
endif
let currline = nextline
let nextline = s:NonComment(+1, currline)
endwhile
" nextline is in the next block or after EOF, so return currline:
return currline
endfun
" Restore options and do some special handling for Operator-pending mode.
" The optional argument is the tail of the matching group.
fun! s:CleanUp(options, mode, ...)
if strlen(a:options)
execute "set" a:options
endif
" Open folds, if appropriate.
if a:mode != "o"
if &foldopen =~ "percent"
normal! zv
endif
" In Operator-pending mode, we want to include the whole match
" (for example, d%).
" This is only a problem if we end up moving in the forward direction.
elseif s:startline < line(".") ||
\ s:startline == line(".") && s:startcol < col(".")
if a:0
" If we want to include the whole line then a:1 should be '$' .
silent! call search(a:1)
endif
endif " a:mode != "o"
return 0
endfun
let &cpo = s:save_cpo
" vim: sts=2 sw=2