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

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