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

Improve/fix indent with closing parenthesis (#129)
[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'], ['else']],
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 let s:special_chars_syn_pattern = "\\vstring|comment|^pythonbytes%(contents)=$|pythonTodo|jedi\\S"
73
74 if !get(g:, 'python_pep8_indent_skip_concealed', 0) || !has('conceal')
75     " Skip strings and comments. Return 1 for chars to skip.
76     " jedi* refers to syntax definitions from jedi-vim for call signatures, which
77     " are inserted temporarily into the buffer.
78     function! s:_skip_special_chars(line, col)
79         return synIDattr(synID(a:line, a:col, 0), 'name')
80               \ =~? s:special_chars_syn_pattern
81     endfunction
82 else
83     " Also ignore anything concealed.
84     " TODO: doc; likely only necessary with jedi-vim, where a better version is
85     " planned (https://github.com/Vimjas/vim-python-pep8-indent/pull/98).
86
87     " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
88     function! s:is_concealed(line, col)
89         let concealed = synconcealed(a:line, a:col)
90         return len(concealed) && concealed[0]
91     endfunction
92
93     function! s:_skip_special_chars(line, col)
94         return synIDattr(synID(a:line, a:col, 0), 'name')
95               \ =~? s:special_chars_syn_pattern
96               \ || s:is_concealed(a:line, a:col)
97     endfunction
98 endif
99
100 " Use 'shiftwidth()' instead of '&sw'.
101 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
102 if exists('*shiftwidth')
103     function! s:sw()
104         return shiftwidth()
105     endfunction
106 else
107     function! s:sw()
108         return &shiftwidth
109     endfunction
110 endif
111
112 " Find backwards the closest open parenthesis/bracket/brace.
113 function! s:find_opening_paren(lnum, col)
114     " Return if cursor is in a comment.
115     if synIDattr(synID(a:lnum, a:col, 0), 'name') =~? 'comment'
116         return [0, 0]
117     endif
118
119     call cursor(a:lnum, a:col)
120
121     let nearest = [0, 0]
122     let timeout = g:python_pep8_indent_searchpair_timeout
123     let skip_special_chars = 's:_skip_special_chars(line("."), col("."))'
124     for [p, maxoff] in items(s:paren_pairs)
125         let stopline = max([0, line('.') - maxoff, nearest[0]])
126         let next = searchpairpos(
127            \ '\V'.p[0], '', '\V'.p[1], 'bnW', skip_special_chars, stopline, timeout)
128         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
129             let nearest = next
130         endif
131     endfor
132     return nearest
133 endfunction
134
135 " Find the start of a multi-line statement
136 function! s:find_start_of_multiline_statement(lnum)
137     let lnum = a:lnum
138     while lnum > 0
139         if getline(lnum - 1) =~# '\\$'
140             let lnum = prevnonblank(lnum - 1)
141         else
142             let [paren_lnum, _] = s:find_opening_paren(lnum, 1)
143             if paren_lnum < 1
144                 return lnum
145             else
146                 let lnum = paren_lnum
147             endif
148         endif
149     endwhile
150 endfunction
151
152 " Find possible indent(s) of the block starter that matches the current line.
153 function! s:find_start_of_block(lnum, types, skip, multiple) abort
154     let r = []
155     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
156     if !empty(a:skip)
157       let re_skip = '\V\^\s\*\('.join(a:skip, '\|').'\)\>'
158     else
159       let re_skip = ''
160     endif
161     let lnum = a:lnum
162     let last_indent = indent(lnum) + 1
163     while lnum > 0 && last_indent > 0
164         let indent = indent(lnum)
165         if indent < last_indent
166             let line = getline(lnum)
167             if !empty(re_skip) && line =~# re_skip
168                 let last_indent = indent
169             elseif line =~# re
170                 if !a:multiple
171                     return [indent]
172                 endif
173                 if index(r, indent) == -1
174                     let r += [indent]
175                 endif
176                 let last_indent = indent
177             endif
178         endif
179         let lnum = prevnonblank(lnum - 1)
180     endwhile
181     return r
182 endfunction
183
184 " Is "expr" true for every position in "lnum", beginning at "start"?
185 " (optionally up to a:1 / 4th argument)
186 function! s:match_expr_on_line(expr, lnum, start, ...)
187     let text = getline(a:lnum)
188     let end = a:0 ? a:1 : len(text)
189     if a:start > end
190         return 1
191     endif
192     let save_pos = getpos('.')
193     let r = 1
194     for i in range(a:start, end)
195         call cursor(a:lnum, i)
196         if !(eval(a:expr) || text[i-1] =~# '\s')
197             let r = 0
198             break
199         endif
200     endfor
201     call setpos('.', save_pos)
202     return r
203 endfunction
204
205 " Line up with open parenthesis/bracket/brace.
206 function! s:indent_like_opening_paren(lnum)
207     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum, 1)
208     if paren_lnum <= 0
209         return -2
210     endif
211     let text = getline(paren_lnum)
212     let base = indent(paren_lnum)
213
214     let nothing_after_opening_paren = s:match_expr_on_line(
215                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
216     let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
217
218     let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
219                 \ get(g:, 'python_pep8_indent_hang_closing', 0))
220
221     if nothing_after_opening_paren
222         if starts_with_closing_paren && !hang_closing
223             let res = base
224         else
225             let res = base + s:sw()
226
227             " Special case for parenthesis.
228             if text[paren_col-1] ==# '(' && getline(a:lnum) !~# '\v\)\s*:?\s*$'
229                 return res
230             endif
231         endif
232     else
233         " Indent to match position of opening paren.
234         let res = paren_col
235     endif
236
237     " If this line is the continuation of a control statement
238     " indent further to distinguish the continuation line
239     " from the next logical line.
240     if text =~# b:control_statement && res == base + s:sw()
241         " But only if not inside parens itself (Flake's E127).
242         let [paren_lnum, _] = s:find_opening_paren(paren_lnum, 1)
243         if paren_lnum <= 0
244             return res + s:sw()
245         endif
246     endif
247     return res
248 endfunction
249
250 " Match indent of first block of this type.
251 function! s:indent_like_block(lnum)
252     let text = getline(a:lnum)
253     for [multiple, block_rules] in [
254                 \ [0, s:block_rules],
255                 \ [1, s:block_rules_multiple],
256                 \ ]
257         for [line_re, blocks_ignore] in items(block_rules)
258             if text !~# line_re
259                 continue
260             endif
261
262             let [blocks, skip] = blocks_ignore
263             let indents = s:find_start_of_block(a:lnum - 1, blocks, skip, multiple)
264             if empty(indents)
265                 return -1
266             endif
267             if len(indents) == 1
268                 return indents[0]
269             endif
270
271             " Multiple valid indents, e.g. for 'else' with both try and if.
272             let indent = indent(a:lnum)
273             if index(indents, indent) != -1
274                 " The indent is valid, keep it.
275                 return indent
276             endif
277             " Fallback to the first/nearest one.
278             return indents[0]
279         endfor
280     endfor
281     return -2
282 endfunction
283
284 function! s:indent_like_previous_line(lnum)
285     let lnum = prevnonblank(a:lnum - 1)
286
287     " No previous line, keep current indent.
288     if lnum < 1
289       return -1
290     endif
291
292     let text = getline(lnum)
293     let start = s:find_start_of_multiline_statement(lnum)
294     let base = indent(start)
295     let current = indent(a:lnum)
296
297     " Ignore last character in previous line?
298     let lastcol = len(text)
299     let col = lastcol
300
301     " Search for final colon that is not inside something to be ignored.
302     while 1
303         if col == 1 | break | endif
304         if text[col-1] =~# '\s' || s:_skip_special_chars(lnum, col)
305             let col = col - 1
306             continue
307         elseif text[col-1] ==# ':'
308             return base + s:sw()
309         endif
310         break
311     endwhile
312
313     if text =~# '\\$' && !s:_skip_special_chars(lnum, lastcol)
314         " If this line is the continuation of a control statement
315         " indent further to distinguish the continuation line
316         " from the next logical line.
317         if getline(start) =~# b:control_statement
318             return base + s:sw() * 2
319         endif
320
321         " Nest (other) explicit continuations only one level deeper.
322         return base + s:sw()
323     endif
324
325     let empty = getline(a:lnum) =~# '^\s*$'
326
327     " Current and prev line are empty, next is not -> indent like next.
328     if empty && a:lnum > 1 &&
329           \ (getline(a:lnum - 1) =~# '^\s*$') &&
330           \ !(getline(a:lnum + 1) =~# '^\s*$')
331       return indent(a:lnum + 1)
332     endif
333
334     " If the previous statement was a stop-execution statement or a pass
335     if getline(start) =~# s:stop_statement
336         " Remove one level of indentation if the user hasn't already dedented
337         if empty || current > base - s:sw()
338             return base - s:sw()
339         endif
340         " Otherwise, trust the user
341         return -1
342     endif
343
344     if (current || !empty) && s:is_dedented_already(current, base)
345         return -1
346     endif
347
348     " In all other cases, line up with the start of the previous statement.
349     return base
350 endfunction
351
352 " If this line is dedented and the number of indent spaces is valid
353 " (multiple of the indentation size), trust the user.
354 function! s:is_dedented_already(current, base)
355     let dedent_size = a:current - a:base
356     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
357 endfunction
358
359 " Is the syntax at lnum (and optionally cnum) a python string?
360 function! s:is_python_string(lnum, ...)
361     let line = getline(a:lnum)
362     if a:0
363       let cols = type(a:1) != type([]) ? [a:1] : a:1
364     else
365       let cols = range(1, max([1, len(line)]))
366     endif
367     for cnum in cols
368         if match(map(synstack(a:lnum, cnum),
369                     \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
370             return 0
371         end
372     endfor
373     return 1
374 endfunction
375
376 function! GetPythonPEPIndent(lnum)
377     " First line has indent 0
378     if a:lnum == 1
379         return 0
380     endif
381
382     let line = getline(a:lnum)
383     let prevline = getline(a:lnum-1)
384
385     " Multilinestrings: continous, docstring or starting.
386     if s:is_python_string(a:lnum-1, max([1, len(prevline)]))
387                 \ && (s:is_python_string(a:lnum, 1)
388                 \     || match(line, '^\%("""\|''''''\)') != -1)
389
390         " Indent closing quotes as the line with the opening ones.
391         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
392         if match_quotes != -1
393             " closing multiline string
394             let quotes = line[match_quotes:(match_quotes+2)]
395             call cursor(a:lnum, 1)
396             let pairpos = searchpairpos(quotes, '', quotes, 'bW', '', 0, g:python_pep8_indent_searchpair_timeout)
397             if pairpos[0] != 0
398                 return indent(pairpos[0])
399             else
400                 return -1
401             endif
402         endif
403
404         if s:is_python_string(a:lnum-1)
405             " Previous line is (completely) a string: keep current indent.
406             return -1
407         endif
408
409         if match(prevline, '^\s*\%("""\|''''''\)') != -1
410             " docstring.
411             return indent(a:lnum-1)
412         endif
413
414         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
415                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
416         if match(prevline, '\v%("""|'''''')$') != -1
417             " Opening multiline string, started in previous line.
418             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
419                         \ || match(line, '\v^\s+$') != -1
420                 " <CR> with empty line or to split up 'foo("""bar' into
421                 " 'foo("""' and 'bar'.
422                 if indent_multi == -2
423                     return indent(a:lnum-1) + s:sw()
424                 endif
425                 return indent_multi
426             endif
427         endif
428
429         " Keep existing indent.
430         if match(line, '\v^\s*\S') != -1
431             return -1
432         endif
433
434         if indent_multi != -2
435             return indent_multi
436         endif
437
438         return s:indent_like_opening_paren(a:lnum)
439     endif
440
441     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
442     let indent = s:indent_like_opening_paren(a:lnum)
443     if indent >= -1
444         return indent
445     endif
446
447     " Blocks: Match indent of first block of this type.
448     let indent = s:indent_like_block(a:lnum)
449     if indent >= -1
450         return indent
451     endif
452
453     return s:indent_like_previous_line(a:lnum)
454 endfunction