]> 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 keeping indent with 'else' for 'if' after 'try-except' (#52)
[etc/vim.git] / indent / python.vim
1 " PEP8 compatible Python indent file
2 " Language:         Python
3 " Maintainer:       Hynek Schlawack <hs@ox.cx>
4 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
5 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
6 " License:          CC0
7 "
8 " vim-python-pep8-indent - A nicer Python indentation style for vim.
9 " Written in 2004 by David Bustos <bustos@caltech.edu>
10 " Maintained from 2004-2005 by Eric Mc Sween <em@tomcom.de>
11 " Maintained from 2013 by Hynek Schlawack <hs@ox.cx>
12 "
13 " To the extent possible under law, the author(s) have dedicated all copyright
14 " and related and neighboring rights to this software to the public domain
15 " worldwide. This software is distributed without any warranty.
16 " You should have received a copy of the CC0 Public Domain Dedication along
17 " with this software. If not, see
18 " <http://creativecommons.org/publicdomain/zero/1.0/>.
19
20 " Only load this indent file when no other was loaded.
21 if exists("b:did_indent")
22     finish
23 endif
24 let b:did_indent = 1
25
26 setlocal expandtab
27 setlocal nolisp
28 setlocal autoindent
29 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
30 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
31 setlocal tabstop=4
32 setlocal softtabstop=4
33 setlocal shiftwidth=4
34
35 let s:maxoff = 50
36 let s:block_rules = {
37             \ '^\s*elif\>': ['if', 'elif'],
38             \ '^\s*except\>': ['try', 'except'],
39             \ '^\s*finally\>': ['try', 'except', 'else']
40             \ }
41 let s:block_rules_multiple = {
42             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
43             \ }
44 let s:paren_pairs = ['()', '{}', '[]']
45 if &ft == 'pyrex' || &ft == 'cython'
46     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
47 else
48     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
49 endif
50 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
51
52 " Skip strings and comments. Return 1 for chars to skip.
53 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
54 " are inserted temporarily into the buffer.
55 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
56             \ '=~? "\\vstring|comment|jedi\\S"'
57
58 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
59             \ '=~? "\\vcomment|jedi\\S"'
60
61 " Also ignore anything concealed.
62 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
63 function! s:is_concealed(line, col)
64     let concealed = synconcealed(a:line, a:col)
65     return len(concealed) && concealed[0]
66 endfunction
67 if has('conceal')
68     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
69 endif
70
71
72 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
73             \ '=~? "comment"'
74
75 " Use 'shiftwidth()' instead of '&sw'.
76 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
77 if exists('*shiftwidth')
78     function! s:sw()
79         return shiftwidth()
80     endfunction
81 else
82     function! s:sw()
83         return &sw
84     endfunction
85 endif
86
87 function! s:pair_sort(x, y)
88     if a:x[0] == a:y[0]
89         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
90     else
91         return a:x[0] > a:y[0] ? 1 : -1
92     endif
93 endfunction
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     let stopline = max([0, line('.') - s:maxoff])
107
108     " Return if cursor is in a comment.
109     exe 'if' s:skip_search '| return [0, 0] | endif'
110
111     let positions = []
112     for p in s:paren_pairs
113         call add(positions, searchpairpos(
114            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline))
115     endfor
116
117     " Remove empty matches and return the type with the closest match
118     call filter(positions, 'v:val[0]')
119     call sort(positions, 's:pair_sort')
120
121     return get(positions, -1, [0, 0])
122 endfunction
123
124 " Find the start of a multi-line statement
125 function! s:find_start_of_multiline_statement(lnum)
126     let lnum = a:lnum
127     while lnum > 0
128         if getline(lnum - 1) =~ '\\$'
129             let lnum = prevnonblank(lnum - 1)
130         else
131             let [paren_lnum, _] = s:find_opening_paren(lnum)
132             if paren_lnum < 1
133                 return lnum
134             else
135                 let lnum = paren_lnum
136             endif
137         endif
138     endwhile
139 endfunction
140
141 " Find possible indent(s) of the block starter that matches the current line.
142 function! s:find_start_of_block(lnum, types, multiple)
143     let r = []
144     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
145     let lnum = a:lnum
146     let last_indent = indent(lnum) + 1
147     while lnum > 0 && last_indent > 0
148         let indent = indent(lnum)
149         if indent < last_indent
150             if getline(lnum) =~# re
151                 if !a:multiple
152                     return [indent]
153                 endif
154                 if !len(r) || index(r, indent) == -1
155                     let r += [indent]
156                 endif
157             endif
158             let last_indent = indent(lnum)
159         endif
160         let lnum = prevnonblank(lnum - 1)
161     endwhile
162     return r
163 endfunction
164
165 " Is "expr" true for every position in "lnum", beginning at "start"?
166 " (optionally up to a:1 / 4th argument)
167 function! s:match_expr_on_line(expr, lnum, start, ...)
168     let text = getline(a:lnum)
169     let end = a:0 ? a:1 : len(text)
170     if a:start > end
171         return 1
172     endif
173     let save_pos = getpos('.')
174     let r = 1
175     for i in range(a:start, end)
176         call cursor(a:lnum, i)
177         if !(eval(a:expr) || text[i-1] =~ '\s')
178             let r = 0
179             break
180         endif
181     endfor
182     call setpos('.', save_pos)
183     return r
184 endfunction
185
186 " Line up with open parenthesis/bracket/brace.
187 function! s:indent_like_opening_paren(lnum)
188     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
189     if paren_lnum <= 0
190         return -2
191     endif
192     let text = getline(paren_lnum)
193     let base = indent(paren_lnum)
194
195     let nothing_after_opening_paren = s:match_expr_on_line(
196                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
197     let starts_with_closing_paren = getline(a:lnum) =~ '^\s*[])}]'
198
199     if nothing_after_opening_paren
200         if starts_with_closing_paren
201             let res = base
202         else
203             let res = base + s:sw()
204         endif
205     else
206         " Indent to match position of opening paren.
207         let res = paren_col
208     endif
209
210     " If this line is the continuation of a control statement
211     " indent further to distinguish the continuation line
212     " from the next logical line.
213     if text =~# b:control_statement && res == base + s:sw()
214         return base + s:sw() * 2
215     else
216         return res
217     endif
218 endfunction
219
220 " Match indent of first block of this type.
221 function! s:indent_like_block(lnum)
222     let text = getline(a:lnum)
223     for [multiple, block_rules] in [
224                 \ [0, s:block_rules],
225                 \ [1, s:block_rules_multiple]]
226         for [line_re, blocks] in items(block_rules)
227             if text !~# line_re
228                 continue
229             endif
230
231             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
232             if !len(indents)
233                 return -1
234             endif
235             if len(indents) == 1
236                 return indents[0]
237             endif
238             " Multiple valid indents, e.g. for 'else' with both try and if.
239             let indent = indent(a:lnum)
240             for possible_indent in indents
241                 if indent == possible_indent
242                     return indent
243                 endif
244             endfor
245             return -2
246         endfor
247     endfor
248     return -2
249 endfunction
250
251 function! s:indent_like_previous_line(lnum)
252     let lnum = prevnonblank(a:lnum - 1)
253
254     " No previous line, keep current indent.
255     if lnum < 1
256       return -1
257     endif
258
259     let text = getline(lnum)
260     let start = s:find_start_of_multiline_statement(lnum)
261     let base = indent(start)
262     let current = indent(a:lnum)
263
264     " Jump to last character in previous line.
265     call cursor(lnum, len(text))
266     let ignore_last_char = eval(s:skip_special_chars)
267
268     " Search for final colon that is not inside something to be ignored.
269     while 1
270         let curpos = getpos(".")[2]
271         if curpos == 1 | break | endif
272         if eval(s:skip_special_chars) || text[curpos-1] =~ '\s'
273             normal! h
274             continue
275         elseif text[curpos-1] == ':'
276             return base + s:sw()
277         endif
278         break
279     endwhile
280
281     if text =~ '\\$' && !ignore_last_char
282         " If this line is the continuation of a control statement
283         " indent further to distinguish the continuation line
284         " from the next logical line.
285         if getline(start) =~# b:control_statement
286             return base + s:sw() * 2
287         endif
288
289         " Nest (other) explicit continuations only one level deeper.
290         return base + s:sw()
291     endif
292
293     " If the previous statement was a stop-execution statement or a pass
294     if getline(start) =~# s:stop_statement
295         " Remove one level of indentation if the user hasn't already dedented
296         if indent(a:lnum) > base - s:sw()
297             return base - s:sw()
298         endif
299         " Otherwise, trust the user
300         return -1
301     endif
302
303     if s:is_dedented_already(current, base)
304         return -1
305     endif
306
307     " In all other cases, line up with the start of the previous statement.
308     return base
309 endfunction
310
311 " If this line is dedented and the number of indent spaces is valid
312 " (multiple of the indentation size), trust the user.
313 function! s:is_dedented_already(current, base)
314     let dedent_size = a:current - a:base
315     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
316 endfunction
317
318 " Is the syntax at lnum (and optionally cnum) a python string?
319 function! s:is_python_string(lnum, ...)
320     let line = getline(a:lnum)
321     let linelen = len(line)
322     if linelen < 1
323       let linelen = 1
324     endif
325     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
326     for cnum in cols
327         if match(map(synstack(a:lnum, cnum),
328                     \ 'synIDattr(v:val,"name")'), 'python\S*String') == -1
329             return 0
330         end
331     endfor
332     return 1
333 endfunction
334
335 function! GetPythonPEPIndent(lnum)
336
337     " First line has indent 0
338     if a:lnum == 1
339         return 0
340     endif
341
342     " Multilinestrings: continous, docstring or starting.
343     if s:is_python_string(a:lnum)
344         if s:is_python_string(a:lnum-1)
345             " Previous line is (completely) a string.
346             return s:indent_like_previous_line(a:lnum)
347         endif
348
349         if match(getline(a:lnum-1), '^\s*\%("""\|''''''\)') != -1
350             " docstring.
351             return s:indent_like_previous_line(a:lnum)
352         endif
353
354         if s:is_python_string(a:lnum-1, len(getline(a:lnum-1)))
355             " String started in previous line.
356             return 0
357         endif
358     endif
359
360     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
361     let indent = s:indent_like_opening_paren(a:lnum)
362     if indent >= -1
363         return indent
364     endif
365
366     " Blocks: Match indent of first block of this type.
367     let indent = s:indent_like_block(a:lnum)
368     if indent >= -1
369         return indent
370     endif
371
372     return s:indent_like_previous_line(a:lnum)
373 endfunction