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

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