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

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