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