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

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