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

s:find_opening_paren: pass lnum/col always, no winsaveview
[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     for [p, maxoff] in items(s:paren_pairs)
122         let stopline = max([0, line('.') - maxoff, nearest[0]])
123         let found = 0
124         while 1
125             let next = searchpairpos(
126                \ '\V'.p[0], '', '\V'.p[1], 'bnW', '', stopline, g:python_pep8_indent_searchpair_timeout)
127
128             if !next[0]
129                 break
130             endif
131             if !s:_skip_special_chars(next[0], next[1])
132                 break
133             endif
134             call cursor(next[0], next[1])
135         endwhile
136         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
137             let nearest = next
138         endif
139     endfor
140     return nearest
141 endfunction
142
143 " Find the start of a multi-line statement
144 function! s:find_start_of_multiline_statement(lnum)
145     let lnum = a:lnum
146     while lnum > 0
147         if getline(lnum - 1) =~# '\\$'
148             let lnum = prevnonblank(lnum - 1)
149         else
150             let [paren_lnum, _] = s:find_opening_paren(lnum, 1)
151             if paren_lnum < 1
152                 return lnum
153             else
154                 let lnum = paren_lnum
155             endif
156         endif
157     endwhile
158 endfunction
159
160 " Find possible indent(s) of the block starter that matches the current line.
161 function! s:find_start_of_block(lnum, types, multiple)
162     let r = []
163     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
164     let lnum = a:lnum
165     let last_indent = indent(lnum) + 1
166     while lnum > 0 && last_indent > 0
167         let indent = indent(lnum)
168         if indent < last_indent
169             if getline(lnum) =~# 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         endif
227     else
228         " Indent to match position of opening paren.
229         let res = paren_col
230     endif
231
232     " If this line is the continuation of a control statement
233     " indent further to distinguish the continuation line
234     " from the next logical line.
235     if text =~# b:control_statement && res == base + s:sw()
236         " But only if not inside parens itself (Flake's E127).
237         let [paren_lnum, _] = s:find_opening_paren(paren_lnum, 1)
238         if paren_lnum <= 0
239             return res + s:sw()
240         endif
241     endif
242     return res
243 endfunction
244
245 " Match indent of first block of this type.
246 function! s:indent_like_block(lnum)
247     let text = getline(a:lnum)
248     for [multiple, block_rules] in [
249                 \ [0, s:block_rules],
250                 \ [1, s:block_rules_multiple]]
251         for [line_re, blocks] in items(block_rules)
252             if text !~# line_re
253                 continue
254             endif
255
256             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
257             if !len(indents)
258                 return -1
259             endif
260             if len(indents) == 1
261                 return indents[0]
262             endif
263
264             " Multiple valid indents, e.g. for 'else' with both try and if.
265             let indent = indent(a:lnum)
266             if index(indents, indent) != -1
267                 " The indent is valid, keep it.
268                 return indent
269             endif
270             " Fallback to the first/nearest one.
271             return indents[0]
272         endfor
273     endfor
274     return -2
275 endfunction
276
277 function! s:indent_like_previous_line(lnum)
278     let lnum = prevnonblank(a:lnum - 1)
279
280     " No previous line, keep current indent.
281     if lnum < 1
282       return -1
283     endif
284
285     let text = getline(lnum)
286     let start = s:find_start_of_multiline_statement(lnum)
287     let base = indent(start)
288     let current = indent(a:lnum)
289
290     " Ignore last character in previous line?
291     let lastcol = len(text)
292     let col = lastcol
293
294     " Search for final colon that is not inside something to be ignored.
295     while 1
296         if col == 1 | break | endif
297         if text[col-1] =~# '\s' || s:_skip_special_chars(lnum, col)
298             let col = col - 1
299             continue
300         elseif text[col-1] ==# ':'
301             return base + s:sw()
302         endif
303         break
304     endwhile
305
306     if text =~# '\\$' && !s:_skip_special_chars(lnum, lastcol)
307         " If this line is the continuation of a control statement
308         " indent further to distinguish the continuation line
309         " from the next logical line.
310         if getline(start) =~# b:control_statement
311             return base + s:sw() * 2
312         endif
313
314         " Nest (other) explicit continuations only one level deeper.
315         return base + s:sw()
316     endif
317
318     let empty = getline(a:lnum) =~# '^\s*$'
319
320     " Current and prev line are empty, next is not -> indent like next.
321     if empty && a:lnum > 1 &&
322           \ (getline(a:lnum - 1) =~# '^\s*$') &&
323           \ !(getline(a:lnum + 1) =~# '^\s*$')
324       return indent(a:lnum + 1)
325     endif
326
327     " If the previous statement was a stop-execution statement or a pass
328     if getline(start) =~# s:stop_statement
329         " Remove one level of indentation if the user hasn't already dedented
330         if empty || current > base - s:sw()
331             return base - s:sw()
332         endif
333         " Otherwise, trust the user
334         return -1
335     endif
336
337     if !empty && s:is_dedented_already(current, base)
338         return -1
339     endif
340
341     " In all other cases, line up with the start of the previous statement.
342     return base
343 endfunction
344
345 " If this line is dedented and the number of indent spaces is valid
346 " (multiple of the indentation size), trust the user.
347 function! s:is_dedented_already(current, base)
348     let dedent_size = a:current - a:base
349     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
350 endfunction
351
352 " Is the syntax at lnum (and optionally cnum) a python string?
353 function! s:is_python_string(lnum, ...)
354     let line = getline(a:lnum)
355     if a:0
356       let cols = type(a:1) != type([]) ? [a:1] : a:1
357     else
358       let cols = range(1, max([1, len(line)]))
359     endif
360     for cnum in cols
361         if match(map(synstack(a:lnum, cnum),
362                     \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
363             return 0
364         end
365     endfor
366     return 1
367 endfunction
368
369 function! GetPythonPEPIndent(lnum)
370     " First line has indent 0
371     if a:lnum == 1
372         return 0
373     endif
374
375     let line = getline(a:lnum)
376     let prevline = getline(a:lnum-1)
377
378     " Multilinestrings: continous, docstring or starting.
379     if s:is_python_string(a:lnum-1, max([1, len(prevline)]))
380                 \ && (s:is_python_string(a:lnum, 1)
381                 \     || match(line, '^\%("""\|''''''\)') != -1)
382
383         " Indent closing quotes as the line with the opening ones.
384         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
385         if match_quotes != -1
386             " closing multiline string
387             let quotes = line[match_quotes:(match_quotes+2)]
388             let pairpos = searchpairpos(quotes, '', quotes, 'b', 1, g:python_pep8_indent_searchpair_timeout)
389             if pairpos[0] != 0
390                 return indent(pairpos[0])
391             else
392                 " TODO: test to cover this!
393             endif
394         endif
395
396         if s:is_python_string(a:lnum-1)
397             " Previous line is (completely) a string: keep current indent.
398             return -1
399         endif
400
401         if match(prevline, '^\s*\%("""\|''''''\)') != -1
402             " docstring.
403             return indent(a:lnum-1)
404         endif
405
406         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
407                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
408         if match(prevline, '\v%("""|'''''')$') != -1
409             " Opening multiline string, started in previous line.
410             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
411                         \ || match(line, '\v^\s+$') != -1
412                 " <CR> with empty line or to split up 'foo("""bar' into
413                 " 'foo("""' and 'bar'.
414                 if indent_multi == -2
415                     return indent(a:lnum-1) + s:sw()
416                 endif
417                 return indent_multi
418             endif
419         endif
420
421         " Keep existing indent.
422         if match(line, '\v^\s*\S') != -1
423             return -1
424         endif
425
426         if indent_multi != -2
427             return indent_multi
428         endif
429
430         return s:indent_like_opening_paren(a:lnum)
431     endif
432
433     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
434     let indent = s:indent_like_opening_paren(a:lnum)
435     if indent >= -1
436         return indent
437     endif
438
439     " Blocks: Match indent of first block of this type.
440     let indent = s:indent_like_block(a:lnum)
441     if indent >= -1
442         return indent
443     endif
444
445     return s:indent_like_previous_line(a:lnum)
446 endfunction