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

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