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