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

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