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

Improve handling of multiline strings (#50)
[etc/vim.git] / indent / python.vim
index 69128996b7ca1b1c0963b456d2b1340013b50d31..6bf45a8bf20405e2d51b47c66464fb4817671ce5 100644 (file)
@@ -32,20 +32,46 @@ setlocal tabstop=4
 setlocal softtabstop=4
 setlocal shiftwidth=4
 
 setlocal softtabstop=4
 setlocal shiftwidth=4
 
+if !exists('g:python_pep8_indent_multiline_string')
+    let g:python_pep8_indent_multiline_string = 0
+endif
+
 let s:maxoff = 50
 let s:block_rules = {
             \ '^\s*elif\>': ['if', 'elif'],
 let s:maxoff = 50
 let s:block_rules = {
             \ '^\s*elif\>': ['if', 'elif'],
-            \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
             \ '^\s*except\>': ['try', 'except'],
             \ '^\s*finally\>': ['try', 'except', 'else']
             \ }
             \ '^\s*except\>': ['try', 'except'],
             \ '^\s*finally\>': ['try', 'except', 'else']
             \ }
+let s:block_rules_multiple = {
+            \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
+            \ }
 let s:paren_pairs = ['()', '{}', '[]']
 let s:paren_pairs = ['()', '{}', '[]']
-let s:control_statement = '^\s*\(if\|while\|with\|for\|except\)\>'
+if &ft == 'pyrex' || &ft == '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\)\>'
 
 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
 
-" Skip strings and comments
+" 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") ' .
 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
-            \ '=~? "string\\|comment"'
+            \ '=~? "\\vstring|comment|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"'
 
 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
             \ '=~? "comment"'
@@ -116,22 +142,49 @@ function! s:find_start_of_multiline_statement(lnum)
     endwhile
 endfunction
 
     endwhile
 endfunction
 
-" Find the block starter that matches the current line
-function! s:find_start_of_block(lnum, types)
+" 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 re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
-
     let lnum = a:lnum
     let last_indent = indent(lnum) + 1
     while lnum > 0 && last_indent > 0
     let lnum = a:lnum
     let last_indent = indent(lnum) + 1
     while lnum > 0 && last_indent > 0
-        if indent(lnum) < last_indent
+        let indent = indent(lnum)
+        if indent < last_indent
             if getline(lnum) =~# re
             if getline(lnum) =~# re
-                return lnum
+                if !a:multiple
+                    return [indent]
+                endif
+                if !len(r) || index(r, indent) == -1
+                    let r += [indent]
+                endif
             endif
             let last_indent = indent(lnum)
         endif
         let lnum = prevnonblank(lnum - 1)
     endwhile
             endif
             let last_indent = indent(lnum)
         endif
         let lnum = prevnonblank(lnum - 1)
     endwhile
-    return 0
+    return r
+endfunction
+
+" 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
+    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
+    endfor
+    call setpos('.', save_pos)
+    return r
 endfunction
 
 " Line up with open parenthesis/bracket/brace.
 endfunction
 
 " Line up with open parenthesis/bracket/brace.
@@ -143,7 +196,8 @@ function! s:indent_like_opening_paren(lnum)
     let text = getline(paren_lnum)
     let base = indent(paren_lnum)
 
     let text = getline(paren_lnum)
     let base = indent(paren_lnum)
 
-    let nothing_after_opening_paren = text =~ '\%'.(paren_col + 1).'c\s*$'
+    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 nothing_after_opening_paren
     let starts_with_closing_paren = getline(a:lnum) =~ '^\s*[])}]'
 
     if nothing_after_opening_paren
@@ -160,7 +214,7 @@ function! s:indent_like_opening_paren(lnum)
     " If this line is the continuation of a control statement
     " indent further to distinguish the continuation line
     " from the next logical line.
     " If this line is the continuation of a control statement
     " indent further to distinguish the continuation line
     " from the next logical line.
-    if text =~# s:control_statement && res == base + s:sw()
+    if text =~# b:control_statement && res == base + s:sw()
         return base + s:sw() * 2
     else
         return res
         return base + s:sw() * 2
     else
         return res
@@ -170,20 +224,31 @@ endfunction
 " Match indent of first block of this type.
 function! s:indent_like_block(lnum)
     let text = getline(a:lnum)
 " 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
 
 
-    for [line_re, blocks] in items(s:block_rules)
-        if text !~# line_re
-            continue
-        endif
-
-        let lnum = s:find_start_of_block(a:lnum - 1, blocks)
-        if lnum > 0
-            return indent(lnum)
-        else
-            return -1
-        endif
+            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)
+            for possible_indent in indents
+                if indent == possible_indent
+                    return indent
+                endif
+            endfor
+            return -2
+        endfor
     endfor
     endfor
-
     return -2
 endfunction
 
     return -2
 endfunction
 
@@ -204,20 +269,24 @@ function! s:indent_like_previous_line(lnum)
     call cursor(lnum, len(text))
     let ignore_last_char = eval(s:skip_special_chars)
 
     call cursor(lnum, len(text))
     let ignore_last_char = eval(s:skip_special_chars)
 
-    " Search for final colon that is not inside a string or comment.
-    while search(':\s*\%(#.*\)\?$', 'bcW', lnum)
-      if eval(s:skip_special_chars)
-        normal! h
-      else
-        return base + s:sw()
-      endif
+    " 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
+        break
     endwhile
 
     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.
     endwhile
 
     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) =~# s:control_statement
+        if getline(start) =~# b:control_statement
             return base + s:sw() * 2
         endif
 
             return base + s:sw() * 2
         endif
 
@@ -235,10 +304,7 @@ function! s:indent_like_previous_line(lnum)
         return -1
     endif
 
         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 = current - base
-    if dedent_size < 0 && current % s:sw() == 0
+    if s:is_dedented_already(current, base)
         return -1
     endif
 
         return -1
     endif
 
@@ -246,6 +312,13 @@ function! s:indent_like_previous_line(lnum)
     return base
 endfunction
 
     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)
 " Is the syntax at lnum (and optionally cnum) a python string?
 function! s:is_python_string(lnum, ...)
     let line = getline(a:lnum)
@@ -264,28 +337,40 @@ function! s:is_python_string(lnum, ...)
 endfunction
 
 function! GetPythonPEPIndent(lnum)
 endfunction
 
 function! GetPythonPEPIndent(lnum)
-
     " First line has indent 0
     if a:lnum == 1
         return 0
     endif
 
     " Multilinestrings: continous, docstring or starting.
     " First line has indent 0
     if a:lnum == 1
         return 0
     endif
 
     " Multilinestrings: continous, docstring or starting.
-    if s:is_python_string(a:lnum)
+    if s:is_python_string(a:lnum, 1)
+                \ && s:is_python_string(a:lnum-1, len(getline(a:lnum-1)))
+        " Keep existing indent.
+        if match(getline(a:lnum), '\v^\s*\S') != -1
+            return -1
+        endif
+
         if s:is_python_string(a:lnum-1)
             " Previous line is (completely) a string.
         if s:is_python_string(a:lnum-1)
             " Previous line is (completely) a string.
-            return s:indent_like_previous_line(a:lnum)
+            return indent(a:lnum-1)
         endif
 
         if match(getline(a:lnum-1), '^\s*\%("""\|''''''\)') != -1
             " docstring.
         endif
 
         if match(getline(a:lnum-1), '^\s*\%("""\|''''''\)') != -1
             " docstring.
-            return s:indent_like_previous_line(a:lnum)
+            return indent(a:lnum-1)
         endif
 
         endif
 
-        if s:is_python_string(a:lnum-1, len(getline(a:lnum-1)))
-            " String started in previous line.
-            return 0
+        let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
+                    \ get(g:, 'python_pep8_indent_multiline_string', 0))
+        if indent_multi != -2
+            return indent_multi
+        endif
+
+        if match(getline(a:lnum-1), '\v%("""|'''''')$') != -1
+            " Opening multiline string, started in previous line.
+            return indent(a:lnum-1) + s:sw()
         endif
         endif
+        return s:indent_like_opening_paren(a:lnum)
     endif
 
     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
     endif
 
     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.