]> git.madduck.net Git - etc/vim.git/blobdiff - 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:

Keep/handle (auto)indent with O (#119)
[etc/vim.git] / indent / python.vim
index 4d1edc2310908c6cd2f36160c7349b6435a756e0..6f846371c218beca24a0184d0bdbddb196a757d7 100644 (file)
 " PEP8 compatible Python indent file
 " Language:         Python
 " PEP8 compatible Python indent file
 " Language:         Python
-" Maintainer:       Hynek Schlawack <hs@ox.cx>
+" Maintainer:       Daniel Hahler <https://daniel.hahler.de/>
+" Prev Maintainer:  Hynek Schlawack <hs@ox.cx>
 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
-" License:          Public Domain
+" License:          CC0
+"
+" vim-python-pep8-indent - A nicer Python indentation style for vim.
+" Written in 2004 by David Bustos <bustos@caltech.edu>
+" Maintained from 2004-2005 by Eric Mc Sween <em@tomcom.de>
+" Maintained from 2013 by Hynek Schlawack <hs@ox.cx>
+" Maintained from 2017 by Daniel Hahler <https://daniel.hahler.de/>
+"
+" To the extent possible under law, the author(s) have dedicated all copyright
+" and related and neighboring rights to this software to the public domain
+" worldwide. This software is distributed without any warranty.
+" You should have received a copy of the CC0 Public Domain Dedication along
+" with this software. If not, see
+" <http://creativecommons.org/publicdomain/zero/1.0/>.
 
 " Only load this indent file when no other was loaded.
 
 " Only load this indent file when no other was loaded.
-if exists("b:did_indent")
+if exists('b:did_indent')
     finish
 endif
 let b:did_indent = 1
 
     finish
 endif
 let b:did_indent = 1
 
-setlocal expandtab
 setlocal nolisp
 setlocal autoindent
 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
 
 setlocal nolisp
 setlocal autoindent
 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
 
-let s:maxoff = 50
+if !exists('g:python_pep8_indent_multiline_string')
+    let g:python_pep8_indent_multiline_string = 0
+endif
+
+if !exists('g:python_pep8_indent_hang_closing')
+    let g:python_pep8_indent_hang_closing = 0
+endif
+
+let s:block_rules = {
+            \ '^\s*elif\>': ['if', 'elif'],
+            \ '^\s*except\>': ['try', 'except'],
+            \ '^\s*finally\>': ['try', 'except', 'else']
+            \ }
+let s:block_rules_multiple = {
+            \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
+            \ }
+" Pairs to look for when searching for opening parenthesis.
+" The value is the maximum offset in lines.
+let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
+
+if &filetype ==# 'pyrex' || &filetype ==# 'cython'
+    let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
+else
+    let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
+endif
+let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
+
+" Skip strings and comments. Return 1 for chars to skip.
+" jedi* refers to syntax definitions from jedi-vim for call signatures, which
+" are inserted temporarily into the buffer.
+let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
+            \ '=~? "\\vstring|comment|^pythonbytes%(contents)=$|jedi\\S"'
+
+let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
+            \ '=~? "\\vcomment|jedi\\S"'
+
+" Also ignore anything concealed.
+" Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
+function! s:is_concealed(line, col)
+    let concealed = synconcealed(a:line, a:col)
+    return len(concealed) && concealed[0]
+endfunction
+if has('conceal')
+    let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
+endif
+
+
+let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
+            \ '=~? "comment"'
+
+" Use 'shiftwidth()' instead of '&sw'.
+" (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
+if exists('*shiftwidth')
+    function! s:sw()
+        return shiftwidth()
+    endfunction
+else
+    function! s:sw()
+        return &shiftwidth
+    endfunction
+endif
 
 " Find backwards the closest open parenthesis/bracket/brace.
 
 " Find backwards the closest open parenthesis/bracket/brace.
-function! s:SearchParensPair()
-    let line = line('.')
-    let col = col('.')
-
-    " Skip strings and comments and don't look too far
-    let skip = "line('.') < " . (line - s:maxoff) . " ? dummy :" .
-                \ 'synIDattr(synID(line("."), col("."), 0), "name") =~? ' .
-                \ '"string\\|comment"'
-
-    " Search for parentheses
-    call cursor(line, col)
-    let parlnum = searchpair('(', '', ')', 'bW', skip)
-    let parcol = col('.')
-
-    " Search for brackets
-    call cursor(line, col)
-    let par2lnum = searchpair('\[', '', '\]', 'bW', skip)
-    let par2col = col('.')
-
-    " Search for braces
-    call cursor(line, col)
-    let par3lnum = searchpair('{', '', '}', 'bW', skip)
-    let par3col = col('.')
-
-    " Get the closest match
-    if par2lnum > parlnum || (par2lnum == parlnum && par2col > parcol)
-        let parlnum = par2lnum
-        let parcol = par2col
-    endif
-    if par3lnum > parlnum || (par3lnum == parlnum && par3col > parcol)
-        let parlnum = par3lnum
-        let parcol = par3col
+function! s:find_opening_paren(...)
+    " optional arguments: line and column (defaults to 1) to search around
+    if a:0 > 0
+        let view = winsaveview()
+        call cursor(a:1, a:0 > 1 ? a:2 : 1)
+        let ret = s:find_opening_paren()
+        call winrestview(view)
+        return ret
     endif
 
     endif
 
-    " Put the cursor on the match
-    if parlnum > 0
-        call cursor(parlnum, parcol)
-    endif
-    return parlnum
+    " Return if cursor is in a comment.
+    exe 'if' s:skip_search '| return [0, 0] | endif'
+
+    let nearest = [0, 0]
+    for [p, maxoff] in items(s:paren_pairs)
+        let stopline = max([0, line('.') - maxoff, nearest[0]])
+        let next = searchpairpos(
+           \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline)
+        if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
+            let nearest = next
+        endif
+    endfor
+    return nearest
 endfunction
 
 " Find the start of a multi-line statement
 endfunction
 
 " Find the start of a multi-line statement
-function! s:StatementStart(lnum)
+function! s:find_start_of_multiline_statement(lnum)
     let lnum = a:lnum
     let lnum = a:lnum
-    while 1
-        if getline(lnum - 1) =~ '\\$'
-            let lnum = lnum - 1
+    while lnum > 0
+        if getline(lnum - 1) =~# '\\$'
+            let lnum = prevnonblank(lnum - 1)
         else
         else
-            call cursor(lnum, 1)
-            let maybe_lnum = s:SearchParensPair()
-            if maybe_lnum < 1
+            let [paren_lnum, _] = s:find_opening_paren(lnum)
+            if paren_lnum < 1
                 return lnum
             else
                 return lnum
             else
-                let lnum = maybe_lnum
+                let lnum = paren_lnum
             endif
         endif
     endwhile
 endfunction
 
             endif
         endif
     endwhile
 endfunction
 
-" Find the block starter that matches the current line
-function! s:BlockStarter(lnum, block_start_re)
+" Find possible indent(s) of the block starter that matches the current line.
+function! s:find_start_of_block(lnum, types, multiple)
+    let r = []
+    let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
     let lnum = a:lnum
     let lnum = a:lnum
-    let maxindent = 10000       " whatever
-    while lnum > 1
-        let lnum = prevnonblank(lnum - 1)
-        if indent(lnum) < maxindent
-            if getline(lnum) =~ a:block_start_re
-                return lnum
-            else
-                let maxindent = indent(lnum)
-                " It's not worth going further if we reached the top level
-                if maxindent == 0
-                    return -1
+    let last_indent = indent(lnum) + 1
+    while lnum > 0 && last_indent > 0
+        let indent = indent(lnum)
+        if indent < last_indent
+            if getline(lnum) =~# re
+                if !a:multiple
+                    return [indent]
                 endif
                 endif
+                if index(r, indent) == -1
+                    let r += [indent]
+                endif
+                let last_indent = indent
             endif
         endif
             endif
         endif
+        let lnum = prevnonblank(lnum - 1)
     endwhile
     endwhile
-    return -1
+    return r
 endfunction
 
 endfunction
 
-function! GetPythonPEPIndent(lnum)
-
-    " First line has indent 0
-    if a:lnum == 1
-        return 0
+" Is "expr" true for every position in "lnum", beginning at "start"?
+" (optionally up to a:1 / 4th argument)
+function! s:match_expr_on_line(expr, lnum, start, ...)
+    let text = getline(a:lnum)
+    let end = a:0 ? a:1 : len(text)
+    if a:start > end
+        return 1
     endif
     endif
-
-    " If we can find an open parenthesis/bracket/brace, line up with it.
-    call cursor(a:lnum, 1)
-    let parlnum = s:SearchParensPair()
-    if parlnum > 0
-        let parcol = col('.')
-        let closing_paren = match(getline(a:lnum), '^\s*[])}]') != -1
-        if match(getline(parlnum), '[([{]\s*$', parcol - 1) != -1
-            if closing_paren
-                return indent(parlnum)
-            else
-                return indent(parlnum) + &shiftwidth
-            endif
-        else
-            return parcol
+    let save_pos = getpos('.')
+    let r = 1
+    for i in range(a:start, end)
+        call cursor(a:lnum, i)
+        if !(eval(a:expr) || text[i-1] =~# '\s')
+            let r = 0
+            break
         endif
         endif
+    endfor
+    call setpos('.', save_pos)
+    return r
+endfunction
+
+" Line up with open parenthesis/bracket/brace.
+function! s:indent_like_opening_paren(lnum)
+    let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
+    if paren_lnum <= 0
+        return -2
     endif
     endif
+    let text = getline(paren_lnum)
+    let base = indent(paren_lnum)
 
 
-    " Examine this line
-    let thisline = getline(a:lnum)
-    let thisindent = indent(a:lnum)
+    let nothing_after_opening_paren = s:match_expr_on_line(
+                \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
+    let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
 
 
-    " If the line starts with 'elif' or 'else', line up with 'if' or 'elif'
-    if thisline =~ '^\s*\(elif\|else\)\>'
-        let bslnum = s:BlockStarter(a:lnum, '^\s*\(if\|elif\)\>')
-        if bslnum > 0
-            return indent(bslnum)
+    let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
+                \ get(g:, 'python_pep8_indent_hang_closing', 0))
+
+    if nothing_after_opening_paren
+        if starts_with_closing_paren && !hang_closing
+            let res = base
         else
         else
-            return -1
+            let res = base + s:sw()
         endif
         endif
+    else
+        " Indent to match position of opening paren.
+        let res = paren_col
     endif
 
     endif
 
-    " If the line starts with 'except' or 'finally', line up with 'try'
-    " or 'except'
-    if thisline =~ '^\s*\(except\|finally\)\>'
-        let bslnum = s:BlockStarter(a:lnum, '^\s*\(try\|except\)\>')
-        if bslnum > 0
-            return indent(bslnum)
-        else
-            return -1
+    " If this line is the continuation of a control statement
+    " indent further to distinguish the continuation line
+    " from the next logical line.
+    if text =~# b:control_statement && res == base + s:sw()
+        " But only if not inside parens itself (Flake's E127).
+        let [paren_lnum, _] = s:find_opening_paren(paren_lnum)
+        if paren_lnum <= 0
+            return res + s:sw()
         endif
     endif
         endif
     endif
+    return res
+endfunction
 
 
-    " Examine previous line
-    let plnum = a:lnum - 1
-    let pline = getline(plnum)
-    let sslnum = s:StatementStart(plnum)
+" Match indent of first block of this type.
+function! s:indent_like_block(lnum)
+    let text = getline(a:lnum)
+    for [multiple, block_rules] in [
+                \ [0, s:block_rules],
+                \ [1, s:block_rules_multiple]]
+        for [line_re, blocks] in items(block_rules)
+            if text !~# line_re
+                continue
+            endif
 
 
-    " If the previous line is blank, keep the same indentation
-    if pline =~ '^\s*$'
-        return -1
+            let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
+            if !len(indents)
+                return -1
+            endif
+            if len(indents) == 1
+                return indents[0]
+            endif
+
+            " Multiple valid indents, e.g. for 'else' with both try and if.
+            let indent = indent(a:lnum)
+            if index(indents, indent) != -1
+                " The indent is valid, keep it.
+                return indent
+            endif
+            " Fallback to the first/nearest one.
+            return indents[0]
+        endfor
+    endfor
+    return -2
+endfunction
+
+function! s:indent_like_previous_line(lnum)
+    let lnum = prevnonblank(a:lnum - 1)
+
+    " No previous line, keep current indent.
+    if lnum < 1
+      return -1
     endif
 
     endif
 
-    " If this line is explicitly joined, find the first indentation that is a
-    " multiple of four and will distinguish itself from next logical line.
-    if pline =~ '\\$'
-        let maybe_indent = indent(sslnum) + &sw
-        let control_structure = '^\s*\(if\|while\|for\s.*\sin\|except\)\s*'
-        if match(getline(sslnum), control_structure) != -1
-            " add extra indent to avoid E125
-            return maybe_indent + &sw
-        else
-            " control structure not found
-            return maybe_indent
+    let text = getline(lnum)
+    let start = s:find_start_of_multiline_statement(lnum)
+    let base = indent(start)
+    let current = indent(a:lnum)
+
+    " Jump to last character in previous line.
+    call cursor(lnum, len(text))
+    let ignore_last_char = eval(s:skip_special_chars)
+
+    " Search for final colon that is not inside something to be ignored.
+    while 1
+        let curpos = getpos('.')[2]
+        if curpos == 1 | break | endif
+        if eval(s:skip_special_chars) || text[curpos-1] =~# '\s'
+            normal! h
+            continue
+        elseif text[curpos-1] ==# ':'
+            return base + s:sw()
         endif
         endif
-    endif
+        break
+    endwhile
 
 
-    " If the previous line ended with a colon and is not a comment, indent
-    " relative to statement start.
-    let pline = substitute(pline, '\\\\', '', 'g')
-    if v:version > 703 || (v:version == 703 && has('patch1037'))
-        let pline = substitute(pline, '".\{-}\\\@1<!"\|''.\{-}\\\@1<!''', '', 'g')
-    else
-        let pline = substitute(pline, '".\{-}\\\@<!"\|''.\{-}\\\@<!''', '', 'g')
+    if text =~# '\\$' && !ignore_last_char
+        " If this line is the continuation of a control statement
+        " indent further to distinguish the continuation line
+        " from the next logical line.
+        if getline(start) =~# b:control_statement
+            return base + s:sw() * 2
+        endif
+
+        " Nest (other) explicit continuations only one level deeper.
+        return base + s:sw()
     endif
     endif
-    if pline =~ '^[^#]*:\s*\(#.*\)\?$'
-        return indent(sslnum) + &sw
+
+    let empty = getline(a:lnum) =~# '^\s*$'
+
+    " Current and prev line are empty, next is not -> indent like next.
+    if empty && a:lnum > 1 &&
+          \ (getline(a:lnum - 1) =~# '^\s*$') &&
+          \ !(getline(a:lnum + 1) =~# '^\s*$')
+      return indent(a:lnum + 1)
     endif
 
     endif
 
-    " If the previous line was a stop-execution statement or a pass
-    if getline(sslnum) =~ '^\s*\(break\|continue\|raise\|return\|pass\)\>'
-        " See if the user has already dedented
-        if indent(a:lnum) > indent(sslnum) - &sw
-            " If not, recommend one dedent
-            return indent(sslnum) - &sw
+    " If the previous statement was a stop-execution statement or a pass
+    if getline(start) =~# s:stop_statement
+        " Remove one level of indentation if the user hasn't already dedented
+        if empty || current > base - s:sw()
+            return base - s:sw()
         endif
         " Otherwise, trust the user
         return -1
     endif
 
         endif
         " Otherwise, trust the user
         return -1
     endif
 
-    " If this line is dedented and the number of indent spaces is valid
-    " (multiple of the indentation size), trust the user
-    let dedent_size = thisindent - indent(plnum)
-    if dedent_size < 0 && thisindent % &sw == 0
+    if (current || !empty) && s:is_dedented_already(current, base)
         return -1
     endif
 
     " In all other cases, line up with the start of the previous statement.
         return -1
     endif
 
     " In all other cases, line up with the start of the previous statement.
-    return indent(sslnum)
+    return base
+endfunction
+
+" If this line is dedented and the number of indent spaces is valid
+" (multiple of the indentation size), trust the user.
+function! s:is_dedented_already(current, base)
+    let dedent_size = a:current - a:base
+    return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
+endfunction
+
+" Is the syntax at lnum (and optionally cnum) a python string?
+function! s:is_python_string(lnum, ...)
+    let line = getline(a:lnum)
+    if a:0
+      let cols = type(a:1) != type([]) ? [a:1] : a:1
+    else
+      let cols = range(1, max([1, len(line)]))
+    endif
+    for cnum in cols
+        if match(map(synstack(a:lnum, cnum),
+                    \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
+            return 0
+        end
+    endfor
+    return 1
+endfunction
+
+function! GetPythonPEPIndent(lnum)
+    " First line has indent 0
+    if a:lnum == 1
+        return 0
+    endif
+
+    let line = getline(a:lnum)
+    let prevline = getline(a:lnum-1)
+
+    " Multilinestrings: continous, docstring or starting.
+    if s:is_python_string(a:lnum-1, max([1, len(prevline)]))
+                \ && (s:is_python_string(a:lnum, 1)
+                \     || match(line, '^\%("""\|''''''\)') != -1)
+
+        " Indent closing quotes as the line with the opening ones.
+        let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
+        if match_quotes != -1
+            " closing multiline string
+            let quotes = line[match_quotes:(match_quotes+2)]
+            let pairpos = searchpairpos(quotes, '', quotes, 'b')
+            if pairpos[0] != 0
+                return indent(pairpos[0])
+            else
+                " TODO: test to cover this!
+            endif
+        endif
+
+        if s:is_python_string(a:lnum-1)
+            " Previous line is (completely) a string: keep current indent.
+            return -1
+        endif
+
+        if match(prevline, '^\s*\%("""\|''''''\)') != -1
+            " docstring.
+            return indent(a:lnum-1)
+        endif
+
+        let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
+                    \ get(g:, 'python_pep8_indent_multiline_string', 0))
+        if match(prevline, '\v%("""|'''''')$') != -1
+            " Opening multiline string, started in previous line.
+            if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
+                        \ || match(line, '\v^\s+$') != -1
+                " <CR> with empty line or to split up 'foo("""bar' into
+                " 'foo("""' and 'bar'.
+                if indent_multi == -2
+                    return indent(a:lnum-1) + s:sw()
+                endif
+                return indent_multi
+            endif
+        endif
+
+        " Keep existing indent.
+        if match(line, '\v^\s*\S') != -1
+            return -1
+        endif
+
+        if indent_multi != -2
+            return indent_multi
+        endif
+
+        return s:indent_like_opening_paren(a:lnum)
+    endif
+
+    " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
+    let indent = s:indent_like_opening_paren(a:lnum)
+    if indent >= -1
+        return indent
+    endif
+
+    " Blocks: Match indent of first block of this type.
+    let indent = s:indent_like_block(a:lnum)
+    if indent >= -1
+        return indent
+    endif
+
+    return s:indent_like_previous_line(a:lnum)
 endfunction
 endfunction