]> 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:

Merge pull request #118 from blueyed/searchpair-timeout
[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 " TODO: check required patch for timeout argument, likely lower than 7.3.429 though.
42 if !exists('g:python_pep8_indent_searchpair_timeout')
43     if has('patch-8.0.1483')
44         let g:python_pep8_indent_searchpair_timeout = 150
45     else
46         let g:python_pep8_indent_searchpair_timeout = 0
47     endif
48 endif
49
50 let s:block_rules = {
51             \ '^\s*elif\>': ['if', 'elif'],
52             \ '^\s*except\>': ['try', 'except'],
53             \ '^\s*finally\>': ['try', 'except', 'else']
54             \ }
55 let s:block_rules_multiple = {
56             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
57             \ }
58 " Pairs to look for when searching for opening parenthesis.
59 " The value is the maximum offset in lines.
60 let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
61
62 if &filetype ==# 'pyrex' || &filetype ==# 'cython'
63     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
64 else
65     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
66 endif
67 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
68
69 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
70             \ '=~? "\\vcomment|jedi\\S"'
71
72 if !get(g:, 'python_pep8_indent_skip_concealed', 0) || !has('conceal')
73     " Skip strings and comments. Return 1 for chars to skip.
74     " jedi* refers to syntax definitions from jedi-vim for call signatures, which
75     " are inserted temporarily into the buffer.
76     function! s:_skip_special_chars(line, col)
77         return synIDattr(synID(a:line, a:col, 0), 'name')
78                 \ =~? "\\vstring|comment|^pythonbytes%(contents)=$|jedi\\S"
79     endfunction
80 else
81     " Also ignore anything concealed.
82     " TODO: doc; likely only necessary with jedi-vim, where a better version is
83     " planned (https://github.com/Vimjas/vim-python-pep8-indent/pull/98).
84
85     " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
86     function! s:is_concealed(line, col)
87         let concealed = synconcealed(a:line, a:col)
88         return len(concealed) && concealed[0]
89     endfunction
90
91     function! s:_skip_special_chars(line, col)
92         return synIDattr(synID(a:line, a:col, 0), 'name')
93                 \ =~? "\\vstring|comment|^pythonbytes%(contents)=$|jedi\\S"
94                 \ || s:is_concealed(a:line, a:col)
95     endfunction
96 endif
97
98 " Use 'shiftwidth()' instead of '&sw'.
99 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
100 if exists('*shiftwidth')
101     function! s:sw()
102         return shiftwidth()
103     endfunction
104 else
105     function! s:sw()
106         return &shiftwidth
107     endfunction
108 endif
109
110 " Find backwards the closest open parenthesis/bracket/brace.
111 function! s:find_opening_paren(lnum, col)
112     " Return if cursor is in a comment.
113     if synIDattr(synID(a:lnum, a:col, 0), 'name') =~? 'comment'
114         return [0, 0]
115     endif
116
117     call cursor(a:lnum, a:col)
118
119     let nearest = [0, 0]
120     let timeout = g:python_pep8_indent_searchpair_timeout
121     let skip_special_chars = 's:_skip_special_chars(line("."), col("."))'
122     for [p, maxoff] in items(s:paren_pairs)
123         let stopline = max([0, line('.') - maxoff, nearest[0]])
124         let next = searchpairpos(
125            \ '\V'.p[0], '', '\V'.p[1], 'bnW', skip_special_chars, stopline, timeout)
126         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
127             let nearest = next
128         endif
129     endfor
130     return nearest
131 endfunction
132
133 " Find the start of a multi-line statement
134 function! s:find_start_of_multiline_statement(lnum)
135     let lnum = a:lnum
136     while lnum > 0
137         if getline(lnum - 1) =~# '\\$'
138             let lnum = prevnonblank(lnum - 1)
139         else
140             let [paren_lnum, _] = s:find_opening_paren(lnum, 1)
141             if paren_lnum < 1
142                 return lnum
143             else
144                 let lnum = paren_lnum
145             endif
146         endif
147     endwhile
148 endfunction
149
150 " Find possible indent(s) of the block starter that matches the current line.
151 function! s:find_start_of_block(lnum, types, multiple)
152     let r = []
153     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
154     let lnum = a:lnum
155     let last_indent = indent(lnum) + 1
156     while lnum > 0 && last_indent > 0
157         let indent = indent(lnum)
158         if indent < last_indent
159             if getline(lnum) =~# re
160                 if !a:multiple
161                     return [indent]
162                 endif
163                 if index(r, indent) == -1
164                     let r += [indent]
165                 endif
166                 let last_indent = indent
167             endif
168         endif
169         let lnum = prevnonblank(lnum - 1)
170     endwhile
171     return r
172 endfunction
173
174 " Is "expr" true for every position in "lnum", beginning at "start"?
175 " (optionally up to a:1 / 4th argument)
176 function! s:match_expr_on_line(expr, lnum, start, ...)
177     let text = getline(a:lnum)
178     let end = a:0 ? a:1 : len(text)
179     if a:start > end
180         return 1
181     endif
182     let save_pos = getpos('.')
183     let r = 1
184     for i in range(a:start, end)
185         call cursor(a:lnum, i)
186         if !(eval(a:expr) || text[i-1] =~# '\s')
187             let r = 0
188             break
189         endif
190     endfor
191     call setpos('.', save_pos)
192     return r
193 endfunction
194
195 " Line up with open parenthesis/bracket/brace.
196 function! s:indent_like_opening_paren(lnum)
197     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum, 1)
198     if paren_lnum <= 0
199         return -2
200     endif
201     let text = getline(paren_lnum)
202     let base = indent(paren_lnum)
203
204     let nothing_after_opening_paren = s:match_expr_on_line(
205                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
206     let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
207
208     let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
209                 \ get(g:, 'python_pep8_indent_hang_closing', 0))
210
211     if nothing_after_opening_paren
212         if starts_with_closing_paren && !hang_closing
213             let res = base
214         else
215             let res = base + s:sw()
216         endif
217     else
218         " Indent to match position of opening paren.
219         let res = paren_col
220     endif
221
222     " If this line is the continuation of a control statement
223     " indent further to distinguish the continuation line
224     " from the next logical line.
225     if text =~# b:control_statement && res == base + s:sw()
226         " But only if not inside parens itself (Flake's E127).
227         let [paren_lnum, _] = s:find_opening_paren(paren_lnum, 1)
228         if paren_lnum <= 0
229             return res + s:sw()
230         endif
231     endif
232     return res
233 endfunction
234
235 " Match indent of first block of this type.
236 function! s:indent_like_block(lnum)
237     let text = getline(a:lnum)
238     for [multiple, block_rules] in [
239                 \ [0, s:block_rules],
240                 \ [1, s:block_rules_multiple]]
241         for [line_re, blocks] in items(block_rules)
242             if text !~# line_re
243                 continue
244             endif
245
246             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
247             if !len(indents)
248                 return -1
249             endif
250             if len(indents) == 1
251                 return indents[0]
252             endif
253
254             " Multiple valid indents, e.g. for 'else' with both try and if.
255             let indent = indent(a:lnum)
256             if index(indents, indent) != -1
257                 " The indent is valid, keep it.
258                 return indent
259             endif
260             " Fallback to the first/nearest one.
261             return indents[0]
262         endfor
263     endfor
264     return -2
265 endfunction
266
267 function! s:indent_like_previous_line(lnum)
268     let lnum = prevnonblank(a:lnum - 1)
269
270     " No previous line, keep current indent.
271     if lnum < 1
272       return -1
273     endif
274
275     let text = getline(lnum)
276     let start = s:find_start_of_multiline_statement(lnum)
277     let base = indent(start)
278     let current = indent(a:lnum)
279
280     " Ignore last character in previous line?
281     let lastcol = len(text)
282     let col = lastcol
283
284     " Search for final colon that is not inside something to be ignored.
285     while 1
286         if col == 1 | break | endif
287         if text[col-1] =~# '\s' || s:_skip_special_chars(lnum, col)
288             let col = col - 1
289             continue
290         elseif text[col-1] ==# ':'
291             return base + s:sw()
292         endif
293         break
294     endwhile
295
296     if text =~# '\\$' && !s:_skip_special_chars(lnum, lastcol)
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 (current || !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     if a:0
346       let cols = type(a:1) != type([]) ? [a:1] : a:1
347     else
348       let cols = range(1, max([1, len(line)]))
349     endif
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, max([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             call cursor(a:lnum, 1)
379             let pairpos = searchpairpos(quotes, '', quotes, 'bW', '', 0, g:python_pep8_indent_searchpair_timeout)
380             if pairpos[0] != 0
381                 return indent(pairpos[0])
382             else
383                 return -1
384             endif
385         endif
386
387         if s:is_python_string(a:lnum-1)
388             " Previous line is (completely) a string: keep current indent.
389             return -1
390         endif
391
392         if match(prevline, '^\s*\%("""\|''''''\)') != -1
393             " docstring.
394             return indent(a:lnum-1)
395         endif
396
397         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
398                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
399         if match(prevline, '\v%("""|'''''')$') != -1
400             " Opening multiline string, started in previous line.
401             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
402                         \ || match(line, '\v^\s+$') != -1
403                 " <CR> with empty line or to split up 'foo("""bar' into
404                 " 'foo("""' and 'bar'.
405                 if indent_multi == -2
406                     return indent(a:lnum-1) + s:sw()
407                 endif
408                 return indent_multi
409             endif
410         endif
411
412         " Keep existing indent.
413         if match(line, '\v^\s*\S') != -1
414             return -1
415         endif
416
417         if indent_multi != -2
418             return indent_multi
419         endif
420
421         return s:indent_like_opening_paren(a:lnum)
422     endif
423
424     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
425     let indent = s:indent_like_opening_paren(a:lnum)
426     if indent >= -1
427         return indent
428     endif
429
430     " Blocks: Match indent of first block of this type.
431     let indent = s:indent_like_block(a:lnum)
432     if indent >= -1
433         return indent
434     endif
435
436     return s:indent_like_previous_line(a:lnum)
437 endfunction