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

86155cf085210abc0f7420a9d7d8c9f1739fd4c9
[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:block_rules = {
38             \ '^\s*elif\>': ['if', 'elif'],
39             \ '^\s*except\>': ['try', 'except'],
40             \ '^\s*finally\>': ['try', 'except', 'else']
41             \ }
42 let s:block_rules_multiple = {
43             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
44             \ }
45 " Pairs to look for when searching for opening parenthesis.
46 " The value is the maximum offset in lines.
47 let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
48
49 if &filetype ==# 'pyrex' || &filetype ==# 'cython'
50     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
51 else
52     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
53 endif
54 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
55
56 " Skip strings and comments. Return 1 for chars to skip.
57 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
58 " are inserted temporarily into the buffer.
59 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
60             \ '=~? "\\vstring|comment|jedi\\S"'
61
62 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
63             \ '=~? "\\vcomment|jedi\\S"'
64
65 " Also ignore anything concealed.
66 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
67 function! s:is_concealed(line, col)
68     let concealed = synconcealed(a:line, a:col)
69     return len(concealed) && concealed[0]
70 endfunction
71 if has('conceal')
72     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
73 endif
74
75
76 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
77             \ '=~? "comment"'
78
79 " Use 'shiftwidth()' instead of '&sw'.
80 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
81 if exists('*shiftwidth')
82     function! s:sw()
83         return shiftwidth()
84     endfunction
85 else
86     function! s:sw()
87         return &shiftwidth
88     endfunction
89 endif
90
91 function! s:pair_sort(x, y)
92     if a:x[0] == a:y[0]
93         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
94     else
95         return a:x[0] > a:y[0] ? 1 : -1
96     endif
97 endfunction
98
99 " Find backwards the closest open parenthesis/bracket/brace.
100 function! s:find_opening_paren(...)
101     " optional arguments: line and column (defaults to 1) to search around
102     if a:0 > 0
103         let view = winsaveview()
104         call cursor(a:1, a:0 > 1 ? a:2 : 1)
105         let ret = s:find_opening_paren()
106         call winrestview(view)
107         return ret
108     endif
109
110     " Return if cursor is in a comment.
111     exe 'if' s:skip_search '| return [0, 0] | endif'
112
113     let nearest = [0, 0]
114     for [p, maxoff] in items(s:paren_pairs)
115         let stopline = max([0, line('.') - maxoff, nearest[0]])
116         let next = searchpairpos(
117            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline)
118         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
119             let nearest = next
120         endif
121     endfor
122     return nearest
123 endfunction
124
125 " Find the start of a multi-line statement
126 function! s:find_start_of_multiline_statement(lnum)
127     let lnum = a:lnum
128     while lnum > 0
129         if getline(lnum - 1) =~# '\\$'
130             let lnum = prevnonblank(lnum - 1)
131         else
132             let [paren_lnum, _] = s:find_opening_paren(lnum)
133             if paren_lnum < 1
134                 return lnum
135             else
136                 let lnum = paren_lnum
137             endif
138         endif
139     endwhile
140 endfunction
141
142 " Find possible indent(s) of the block starter that matches the current line.
143 function! s:find_start_of_block(lnum, types, multiple)
144     let r = []
145     let types = copy(a:types)
146     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
147     let lnum = a:lnum
148     let last_indent = indent(lnum) + 1
149     while lnum > 0 && last_indent > 0
150         let indent = indent(lnum)
151         if indent < last_indent
152             for type in types
153                 let re = '\v^\s*'.type.'>'
154                 if getline(lnum) =~# re
155                     if !a:multiple
156                         return [indent]
157                     endif
158                     if index(r, indent) == -1
159                         let r += [indent]
160                     endif
161                     " Remove any handled type, e.g. 'if'.
162                     call remove(types, index(types, type))
163                 endif
164             endfor
165             let last_indent = indent(lnum)
166         endif
167         let lnum = prevnonblank(lnum - 1)
168     endwhile
169     return r
170 endfunction
171
172 " Is "expr" true for every position in "lnum", beginning at "start"?
173 " (optionally up to a:1 / 4th argument)
174 function! s:match_expr_on_line(expr, lnum, start, ...)
175     let text = getline(a:lnum)
176     let end = a:0 ? a:1 : len(text)
177     if a:start > end
178         return 1
179     endif
180     let save_pos = getpos('.')
181     let r = 1
182     for i in range(a:start, end)
183         call cursor(a:lnum, i)
184         if !(eval(a:expr) || text[i-1] =~# '\s')
185             let r = 0
186             break
187         endif
188     endfor
189     call setpos('.', save_pos)
190     return r
191 endfunction
192
193 " Line up with open parenthesis/bracket/brace.
194 function! s:indent_like_opening_paren(lnum)
195     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
196     if paren_lnum <= 0
197         return -2
198     endif
199     let text = getline(paren_lnum)
200     let base = indent(paren_lnum)
201
202     let nothing_after_opening_paren = s:match_expr_on_line(
203                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
204     let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
205
206     if nothing_after_opening_paren
207         if starts_with_closing_paren
208             let res = base
209         else
210             let res = base + s:sw()
211         endif
212     else
213         " Indent to match position of opening paren.
214         let res = paren_col
215     endif
216
217     " If this line is the continuation of a control statement
218     " indent further to distinguish the continuation line
219     " from the next logical line.
220     if text =~# b:control_statement && res == base + s:sw()
221         return base + s:sw() * 2
222     else
223         return res
224     endif
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     let linelen = len(line)
339     if linelen < 1
340       let linelen = 1
341     endif
342     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
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, 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.
381             return indent(a:lnum-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