]> git.madduck.net Git - etc/vim.git/blob - indent/python.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

tests: remove nil from hang-closing tests (#105)
[etc/vim.git] / indent / python.vim
1 " PEP8 compatible Python indent file
2 " Language:         Python
3 " Maintainer:       Daniel Hahler <https://daniel.hahler.de/>
4 " Prev Maintainer:  Hynek Schlawack <hs@ox.cx>
5 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
6 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
7 " License:          CC0
8 "
9 " vim-python-pep8-indent - A nicer Python indentation style for vim.
10 " Written in 2004 by David Bustos <bustos@caltech.edu>
11 " Maintained from 2004-2005 by Eric Mc Sween <em@tomcom.de>
12 " Maintained from 2013 by Hynek Schlawack <hs@ox.cx>
13 " Maintained from 2017 by Daniel Hahler <https://daniel.hahler.de/>
14 "
15 " To the extent possible under law, the author(s) have dedicated all copyright
16 " and related and neighboring rights to this software to the public domain
17 " worldwide. This software is distributed without any warranty.
18 " You should have received a copy of the CC0 Public Domain Dedication along
19 " with this software. If not, see
20 " <http://creativecommons.org/publicdomain/zero/1.0/>.
21
22 " Only load this indent file when no other was loaded.
23 if exists('b:did_indent')
24     finish
25 endif
26 let b:did_indent = 1
27
28 setlocal nolisp
29 setlocal autoindent
30 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
31 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
32
33 if !exists('g:python_pep8_indent_multiline_string')
34     let g:python_pep8_indent_multiline_string = 0
35 endif
36
37 if !exists('g:python_pep8_indent_hang_closing')
38     let g:python_pep8_indent_hang_closing = 0
39 endif
40
41 let s:block_rules = {
42             \ '^\s*elif\>': ['if', 'elif'],
43             \ '^\s*except\>': ['try', 'except'],
44             \ '^\s*finally\>': ['try', 'except', 'else']
45             \ }
46 let s:block_rules_multiple = {
47             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
48             \ }
49 " Pairs to look for when searching for opening parenthesis.
50 " The value is the maximum offset in lines.
51 let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
52
53 if &filetype ==# 'pyrex' || &filetype ==# 'cython'
54     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
55 else
56     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
57 endif
58 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
59
60 " Skip strings and comments. Return 1 for chars to skip.
61 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
62 " are inserted temporarily into the buffer.
63 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
64             \ '=~? "\\vstring|comment|jedi\\S"'
65
66 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
67             \ '=~? "\\vcomment|jedi\\S"'
68
69 " Also ignore anything concealed.
70 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
71 function! s:is_concealed(line, col)
72     let concealed = synconcealed(a:line, a:col)
73     return len(concealed) && concealed[0]
74 endfunction
75 if has('conceal')
76     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
77 endif
78
79
80 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
81             \ '=~? "comment"'
82
83 " Use 'shiftwidth()' instead of '&sw'.
84 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
85 if exists('*shiftwidth')
86     function! s:sw()
87         return shiftwidth()
88     endfunction
89 else
90     function! s:sw()
91         return &shiftwidth
92     endfunction
93 endif
94
95 function! s:pair_sort(x, y)
96     if a:x[0] == a:y[0]
97         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
98     else
99         return a:x[0] > a:y[0] ? 1 : -1
100     endif
101 endfunction
102
103 " Find backwards the closest open parenthesis/bracket/brace.
104 function! s:find_opening_paren(...)
105     " optional arguments: line and column (defaults to 1) to search around
106     if a:0 > 0
107         let view = winsaveview()
108         call cursor(a:1, a:0 > 1 ? a:2 : 1)
109         let ret = s:find_opening_paren()
110         call winrestview(view)
111         return ret
112     endif
113
114     " Return if cursor is in a comment.
115     exe 'if' s:skip_search '| return [0, 0] | endif'
116
117     let nearest = [0, 0]
118     for [p, maxoff] in items(s:paren_pairs)
119         let stopline = max([0, line('.') - maxoff, nearest[0]])
120         let next = searchpairpos(
121            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline)
122         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
123             let nearest = next
124         endif
125     endfor
126     return nearest
127 endfunction
128
129 " Find the start of a multi-line statement
130 function! s:find_start_of_multiline_statement(lnum)
131     let lnum = a:lnum
132     while lnum > 0
133         if getline(lnum - 1) =~# '\\$'
134             let lnum = prevnonblank(lnum - 1)
135         else
136             let [paren_lnum, _] = s:find_opening_paren(lnum)
137             if paren_lnum < 1
138                 return lnum
139             else
140                 let lnum = paren_lnum
141             endif
142         endif
143     endwhile
144 endfunction
145
146 " Find possible indent(s) of the block starter that matches the current line.
147 function! s:find_start_of_block(lnum, types, multiple)
148     let r = []
149     let types = copy(a:types)
150     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
151     let lnum = a:lnum
152     let last_indent = indent(lnum) + 1
153     while lnum > 0 && last_indent > 0
154         let indent = indent(lnum)
155         if indent < last_indent
156             for type in types
157                 let re = '\v^\s*'.type.'>'
158                 if getline(lnum) =~# re
159                     if !a:multiple
160                         return [indent]
161                     endif
162                     if index(r, indent) == -1
163                         let r += [indent]
164                     endif
165                     " Remove any handled type, e.g. 'if'.
166                     call remove(types, index(types, type))
167                 endif
168             endfor
169             let last_indent = indent(lnum)
170         endif
171         let lnum = prevnonblank(lnum - 1)
172     endwhile
173     return r
174 endfunction
175
176 " Is "expr" true for every position in "lnum", beginning at "start"?
177 " (optionally up to a:1 / 4th argument)
178 function! s:match_expr_on_line(expr, lnum, start, ...)
179     let text = getline(a:lnum)
180     let end = a:0 ? a:1 : len(text)
181     if a:start > end
182         return 1
183     endif
184     let save_pos = getpos('.')
185     let r = 1
186     for i in range(a:start, end)
187         call cursor(a:lnum, i)
188         if !(eval(a:expr) || text[i-1] =~# '\s')
189             let r = 0
190             break
191         endif
192     endfor
193     call setpos('.', save_pos)
194     return r
195 endfunction
196
197 " Line up with open parenthesis/bracket/brace.
198 function! s:indent_like_opening_paren(lnum)
199     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
200     if paren_lnum <= 0
201         return -2
202     endif
203     let text = getline(paren_lnum)
204     let base = indent(paren_lnum)
205
206     let nothing_after_opening_paren = s:match_expr_on_line(
207                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
208     let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
209
210     let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
211                 \ get(g:, 'python_pep8_indent_hang_closing', 0))
212
213     if nothing_after_opening_paren
214         if starts_with_closing_paren && !hang_closing
215             let res = base
216         else
217             let res = base + s:sw()
218         endif
219     else
220         " Indent to match position of opening paren.
221         let res = paren_col
222     endif
223
224     " If this line is the continuation of a control statement
225     " indent further to distinguish the continuation line
226     " from the next logical line.
227     if text =~# b:control_statement && res == base + s:sw()
228         return base + s:sw() * 2
229     else
230         return res
231     endif
232 endfunction
233
234 " Match indent of first block of this type.
235 function! s:indent_like_block(lnum)
236     let text = getline(a:lnum)
237     for [multiple, block_rules] in [
238                 \ [0, s:block_rules],
239                 \ [1, s:block_rules_multiple]]
240         for [line_re, blocks] in items(block_rules)
241             if text !~# line_re
242                 continue
243             endif
244
245             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
246             if !len(indents)
247                 return -1
248             endif
249             if len(indents) == 1
250                 return indents[0]
251             endif
252
253             " Multiple valid indents, e.g. for 'else' with both try and if.
254             let indent = indent(a:lnum)
255             if index(indents, indent) != -1
256                 " The indent is valid, keep it.
257                 return indent
258             endif
259             " Fallback to the first/nearest one.
260             return indents[0]
261         endfor
262     endfor
263     return -2
264 endfunction
265
266 function! s:indent_like_previous_line(lnum)
267     let lnum = prevnonblank(a:lnum - 1)
268
269     " No previous line, keep current indent.
270     if lnum < 1
271       return -1
272     endif
273
274     let text = getline(lnum)
275     let start = s:find_start_of_multiline_statement(lnum)
276     let base = indent(start)
277     let current = indent(a:lnum)
278
279     " Jump to last character in previous line.
280     call cursor(lnum, len(text))
281     let ignore_last_char = eval(s:skip_special_chars)
282
283     " Search for final colon that is not inside something to be ignored.
284     while 1
285         let curpos = getpos('.')[2]
286         if curpos == 1 | break | endif
287         if eval(s:skip_special_chars) || text[curpos-1] =~# '\s'
288             normal! h
289             continue
290         elseif text[curpos-1] ==# ':'
291             return base + s:sw()
292         endif
293         break
294     endwhile
295
296     if text =~# '\\$' && !ignore_last_char
297         " If this line is the continuation of a control statement
298         " indent further to distinguish the continuation line
299         " from the next logical line.
300         if getline(start) =~# b:control_statement
301             return base + s:sw() * 2
302         endif
303
304         " Nest (other) explicit continuations only one level deeper.
305         return base + s:sw()
306     endif
307
308     let empty = getline(a:lnum) =~# '^\s*$'
309
310     " Current and prev line are empty, next is not -> indent like next.
311     if empty && a:lnum > 1 &&
312           \ (getline(a:lnum - 1) =~# '^\s*$') &&
313           \ !(getline(a:lnum + 1) =~# '^\s*$')
314       return indent(a:lnum + 1)
315     endif
316
317     " If the previous statement was a stop-execution statement or a pass
318     if getline(start) =~# s:stop_statement
319         " Remove one level of indentation if the user hasn't already dedented
320         if empty || current > base - s:sw()
321             return base - s:sw()
322         endif
323         " Otherwise, trust the user
324         return -1
325     endif
326
327     if !empty && s:is_dedented_already(current, base)
328         return -1
329     endif
330
331     " In all other cases, line up with the start of the previous statement.
332     return base
333 endfunction
334
335 " If this line is dedented and the number of indent spaces is valid
336 " (multiple of the indentation size), trust the user.
337 function! s:is_dedented_already(current, base)
338     let dedent_size = a:current - a:base
339     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
340 endfunction
341
342 " Is the syntax at lnum (and optionally cnum) a python string?
343 function! s:is_python_string(lnum, ...)
344     let line = getline(a:lnum)
345     let linelen = len(line)
346     if linelen < 1
347       let linelen = 1
348     endif
349     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
350     for cnum in cols
351         if match(map(synstack(a:lnum, cnum),
352                     \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
353             return 0
354         end
355     endfor
356     return 1
357 endfunction
358
359 function! GetPythonPEPIndent(lnum)
360     " First line has indent 0
361     if a:lnum == 1
362         return 0
363     endif
364
365     let line = getline(a:lnum)
366     let prevline = getline(a:lnum-1)
367
368     " Multilinestrings: continous, docstring or starting.
369     if s:is_python_string(a:lnum-1, len(prevline))
370                 \ && (s:is_python_string(a:lnum, 1)
371                 \     || match(line, '^\%("""\|''''''\)') != -1)
372
373         " Indent closing quotes as the line with the opening ones.
374         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
375         if match_quotes != -1
376             " closing multiline string
377             let quotes = line[match_quotes:(match_quotes+2)]
378             let pairpos = searchpairpos(quotes, '', quotes, 'b')
379             if pairpos[0] != 0
380                 return indent(pairpos[0])
381             else
382                 " TODO: test to cover this!
383             endif
384         endif
385
386         if s:is_python_string(a:lnum-1)
387             " Previous line is (completely) a string.
388             return indent(a:lnum-1)
389         endif
390
391         if match(prevline, '^\s*\%("""\|''''''\)') != -1
392             " docstring.
393             return indent(a:lnum-1)
394         endif
395
396         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
397                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
398         if match(prevline, '\v%("""|'''''')$') != -1
399             " Opening multiline string, started in previous line.
400             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
401                         \ || match(line, '\v^\s+$') != -1
402                 " <CR> with empty line or to split up 'foo("""bar' into
403                 " 'foo("""' and 'bar'.
404                 if indent_multi == -2
405                     return indent(a:lnum-1) + s:sw()
406                 endif
407                 return indent_multi
408             endif
409         endif
410
411         " Keep existing indent.
412         if match(line, '\v^\s*\S') != -1
413             return -1
414         endif
415
416         if indent_multi != -2
417             return indent_multi
418         endif
419
420         return s:indent_like_opening_paren(a:lnum)
421     endif
422
423     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
424     let indent = s:indent_like_opening_paren(a:lnum)
425     if indent >= -1
426         return indent
427     endif
428
429     " Blocks: Match indent of first block of this type.
430     let indent = s:indent_like_block(a:lnum)
431     if indent >= -1
432         return indent
433     endif
434
435     return s:indent_like_previous_line(a:lnum)
436 endfunction