1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 17:20:04 +08:00
SpaceVim/bundle/vim-pythonsense/autoload/pythonsense.vim

700 lines
26 KiB
VimL

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" File: autoload/pythonsense.vim
" Author: Jeet Sukumaran
"
" Copyright: (C) 2018 Jeet Sukumaran
"
" License: Permission is hereby granted, free of charge, to any person obtaining
" a copy of this software and associated documentation files (the
" "Software"), to deal in the Software without restriction, including
" without limitation the rights to use, copy, modify, merge, publish,
" distribute, sublicense, and/or sell copies of the Software, and to
" permit persons to whom the Software is furnished to do so, subject to
" the following conditions:
"
" The above copyright notice and this permission notice shall be included
" in all copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
" OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
" IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
" CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
" TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
"
" SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"
" Credits: - pythontextobj.vim by Nat Williams (https://github.com/natw/vim-pythontextobj)
" - chapa.vim by Alfredo Deza (https://github.com/alfredodeza/chapa.vim)
" - indentobj by Austin Taylor (https://github.com/austintaylor/vim-indentobject)
" - Python Docstring Text Objects by gfixler (https://pastebin.com/u/gfixler)
" - vim-indent-object by Michael Smith (http://github.com/michaeljsmith/vim-indent-object)
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Support Functions {{{1
function! pythonsense#trawl_search(pattern, start_line, fwd)
let current_line = a:start_line
let lastline = line('$')
if a:fwd
let stepvalue = 1
else
let stepvalue = -1
endif
while (current_line > 0 && current_line <= lastline)
let line_text = getline(current_line)
let m = match(line_text, a:pattern)
if m >= 0
return current_line
endif
let current_line = current_line + stepvalue
endwhile
return 0
endfunction
" }}}1
" Python (Statement) Text Objects {{{1
" Based on:
" - https://github.com/natw/vim-pythontextobj
" - https://github.com/alfredodeza/chapa.vim
" - https://github.com/austintaylor/vim-indentobject
" - http://github.com/michaeljsmith/vim-indent-object
" Select an object ("class"/"function")
let s:pythonsense_obj_start_line = -1
let s:pythonsense_obj_end_line = -1
function! pythonsense#select_named_object(obj_name, inner, range)
" Is this a new selection?
let new_vis = 0
let new_vis = new_vis || s:pythonsense_obj_start_line != a:range[0]
let new_vis = new_vis || s:pythonsense_obj_end_line != a:range[1]
" store current range
let s:pythonsense_obj_start_line = a:range[0]
let s:pythonsense_obj_end_line = a:range[1]
" Repeatedly increase the scope of the selection.
let cnt = 1
let scan_start_line = s:pythonsense_obj_start_line
while scan_start_line > 0
if getline(scan_start_line) !~ '^\s*$'
break
endif
let scan_start_line -= 1
endwhile
if scan_start_line == 0
return [-1, -1]
endif
let obj_max_indent_level = -1
while cnt > 0
let current_line_nr = scan_start_line
let [obj_start_line, obj_end_line] = pythonsense#get_object_line_range(a:obj_name, obj_max_indent_level, current_line_nr, s:pythonsense_obj_end_line, a:inner)
if obj_start_line == -1
return [-1, -1]
endif
let is_changed = 0
let is_changed = is_changed || s:pythonsense_obj_start_line != obj_start_line
let is_changed = is_changed || s:pythonsense_obj_end_line != obj_end_line
if new_vis
let is_changed = 1
endif
let s:pythonsense_obj_start_line = obj_start_line
let s:pythonsense_obj_end_line = obj_end_line
" If there was no change, then don't decrement the count (it didn't
" count because it didn't do anything).
if is_changed
let cnt = cnt - 1
else
" no change to selection;
" move to line above selection and try again
if scan_start_line == 0
return [-1, -1]
endif
let [min_indent, max_indent] = pythonsense#get_minmax_indent_count('\(class\|def\|async def\)', scan_start_line, obj_end_line)
if min_indent == 0
return [-1, -1]
endif
let obj_max_indent_level = min_indent - 1
let scan_start_line -= 1
endif
endwhile
" select range
if obj_end_line >= obj_start_line
exec obj_start_line
execute "normal! V" . obj_end_line . "G"
return [obj_start_line, obj_end_line]
else
return [-1, -1]
endif
endfunction
function! pythonsense#get_object_line_range(obj_name, obj_max_indent_level, line_range_start, line_range_end, inner)
" find definition line
let current_line_nr = a:line_range_start
if a:line_range_start == a:line_range_end
let search_past_decorator_last_line = line("$")
else
let search_past_decorator_last_line = a:line_range_end
endif
while current_line_nr <= search_past_decorator_last_line
if getline(current_line_nr) !~ '^\s*@.*$'
break
end
let current_line_nr += 1
endwhile
if current_line_nr > search_past_decorator_last_line
return [-1, -1]
endif
if current_line_nr > a:line_range_end
let effective_line_range_end = current_line_nr
else
let effective_line_range_end = a:line_range_end
endif
let obj_start_line = pythonsense#get_named_python_obj_start_line_nr(a:obj_name, a:obj_max_indent_level, current_line_nr, 0)
" no object definition line in file
if (! obj_start_line)
return [-1, -1]
endif
let obj_header_line = obj_start_line
let obj_header_indent = pythonsense#get_line_indent_count(obj_header_line)
if obj_header_indent > 0
let obj_header_indent -= 1
endif
let obj_end_line = pythonsense#get_object_end_line_nr(obj_start_line, obj_start_line, a:inner)
" in case of a class definition, the parentheses are optional
if a:obj_name == "def"
let pattern = '^[^#]*)[^#]*:\(\s*$\|\s*#.*$\)'
else
let pattern = '^[^#]*)\?[^#]*:\(\s*$\|\s*#.*$\)'
endif
if (a:inner)
" find class/function body
let inner_start_line = obj_start_line
while inner_start_line <= line('$')
if getline(inner_start_line) =~# pattern
break
endif
let inner_start_line += 1
endwhile
if inner_start_line <= line('$')
let obj_start_line = inner_start_line + 1
endif
else
" include decorators
let dec_line = pythonsense#get_start_decorators_line_nr(obj_start_line)
if dec_line < obj_start_line
let obj_start_line = dec_line
endif
endif
" This is an ugly hack to deal with (some) specially indented cases
" (especially when searching for a 'class' while inside a non-class member
" function, or when searching for a 'def' with nothing but class
" definitions above)
" Make sure there is no statement line with a lower indentation than the
" definition line in between the current line and the definition line
if a:obj_name == 'class'
let pattern = 'def\|async def'
else
let pattern = 'class'
endif
let pattern = '^\s*[^#]*\s*\k'
if pythonsense#is_statement_encountered_between_two_lines(pattern, obj_header_indent, obj_start_line, current_line_nr)
return [-1, -1]
endif
return [obj_start_line, obj_end_line]
endfunction
function! pythonsense#get_object_end_line_nr(obj_start, search_start, inner)
let obj_indent = pythonsense#get_line_indent_count(a:obj_start)
let obj_end = pythonsense#get_next_indent_line_nr(a:search_start, obj_indent)
if a:inner
let obj_end = prevnonblank(obj_end)
endif
return obj_end
endfunction
function! pythonsense#get_next_indent_line_nr(search_start, obj_indent)
let line = a:search_start
" Handle multiline definition
let saved_cursor = getcurpos()
call cursor(line, 0)
normal! f(%
let line = line('.')
call setpos('.', saved_cursor)
let lastline = line('$')
while (line > 0 && line <= lastline)
let line = line + 1
if (pythonsense#get_line_indent_count(line) <= a:obj_indent && getline(line) !~ '^\s*$')
return line - 1
endif
endwhile
return lastline
endfunction
function! pythonsense#get_start_decorators_line_nr(start)
" Returns the line of the first decorator line above the starting line,
" counting only decorators with the same level.
let start_line_indent = pythonsense#get_line_indent_count(a:start)
let last_non_blank_line = a:start
let current_line = a:start - 1
while current_line > 0
if getline(current_line) !~ '^\s*$'
if pythonsense#get_line_indent_count(current_line) != start_line_indent
break
endif
if getline(current_line) !~ '^\s*@'
break
endif
let last_non_blank_line = current_line
endif
let current_line -= 1
endwhile
return last_non_blank_line
endfunction
function! pythonsense#get_line_indent_count(line_nr)
if b:pythonsense_is_tab_indented
let indent_count = matchstrpos(getline(a:line_nr), '^\t\+')[2]
else
let indent_count = indent(a:line_nr)
endif
return indent_count
endfunction
function! pythonsense#get_minmax_indent_count(pattern, line_range_start, line_range_end)
let current_line = a:line_range_start
let min_indent_count = -1
let max_indent_count = -1
while current_line <= a:line_range_end && current_line <= line('$')
if getline(current_line) !~ '^\s*$'
if getline(current_line) =~# a:pattern
let current_indent = pythonsense#get_line_indent_count(current_line)
if min_indent_count < 0 || current_indent < min_indent_count
let min_indent_count = current_indent
endif
if max_indent_count < 0 || current_indent > max_indent_count
let max_indent_count = current_indent
endif
endif
endif
let current_line = current_line + 1
endwhile
return [min_indent_count, max_indent_count]
endfunction
function! pythonsense#is_statement_encountered_between_two_lines(pattern, max_indent, line_range_start, line_range_end)
let current_line = a:line_range_start
while current_line <= a:line_range_end && current_line <= line('$')
if getline(current_line) !~ '^\s*$'
if getline(current_line) =~# a:pattern
let current_indent = pythonsense#get_line_indent_count(current_line)
if a:max_indent > -1 && current_indent < a:max_indent
return 1
endif
endif
endif
let current_line += 1
endwhile
return 0
endfunction
function! pythonsense#get_indent_char()
if b:pythonsense_is_tab_indented
let indent_char = '\t'
else
let indent_char = " "
endif
return indent_char
endfunction
function! pythonsense#get_named_python_obj_start_line_nr(obj_name, obj_max_indent_level, start_line, fwd)
let lastline = line('$')
if a:fwd
let stepvalue = 1
else
let stepvalue = -1
endif
let indent_char = pythonsense#get_indent_char()
let current_line = a:start_line
while (current_line > 0 && current_line <= lastline)
" if getline(current_line) !~ '\(^\s*$\|^\s*[#@].*$\)'
if getline(current_line) !~ '^\s*$'
break
endif
let current_line = current_line + stepvalue
endwhile
if a:obj_max_indent_level > -1
let pattern = '^' . indent_char . '\{0,' . a:obj_max_indent_level . '}' . '\(class\|def\|async def\)'
else
let pattern = '^\s*' . '\(class\|def\|async def\)'
endif
if getline(current_line) =~# pattern
if getline(current_line) =~# a:obj_name
return current_line
endif
endif
let target_line_indent = pythonsense#get_line_indent_count(current_line) - 1
if target_line_indent < 0
let target_line_indent = 0
endif
if a:obj_max_indent_level > -1 && target_line_indent > a:obj_max_indent_level
let target_line_indent = a:obj_max_indent_level
endif
let max_indent = target_line_indent
while (current_line > 0 && current_line <= lastline)
let pattern = '^' . indent_char . '\{0,' . max_indent . '}' . '\(class\|def\|async def\)'
if getline(current_line) =~# pattern
if getline(current_line) =~# a:obj_name
return current_line
else
if a:obj_name != 'class' && pythonsense#get_line_indent_count(current_line) <= max_indent
" encountered a scope block at a lower indent level before
" encountering object definition
return 0
endif
endif
endif
" let m = match(getline(current_line), pattern)
" if m >= 0
" return current_line
" endif
let target_line_indent = pythonsense#get_line_indent_count(current_line) - 1
" Special case for multiline argument lines, with the parameter being
" indented one step more than the open def/class and the closing
" parenthesis.
let closing_pattern = '^' . indent_char . '*)'
if target_line_indent > 0 && target_line_indent < max_indent && getline(current_line) !~# closing_pattern
let max_indent = target_line_indent
endif
if a:obj_max_indent_level > -1 && target_line_indent > a:obj_max_indent_level
let target_line_indent = a:obj_max_indent_level
endif
let current_line = current_line + stepvalue
endwhile
return 0
endfunction
function! pythonsense#python_text_object(obj_name, inner, mode)
if a:mode == "o"
let lnrange = [line("."), line(".")]
else
let lnrange = [line("'<"), line("'>")]
endif
let nreps_left = 1 "v:count1
while nreps_left > 0
let lnrange = pythonsense#select_named_object(a:obj_name, a:inner, lnrange)
if lnrange[0] == -1
break
endif
let s:pythonsense_obj_start_line = lnrange[0]
let s:pythonsense_obj_end_line = lnrange[1]
let nreps_left -= 1
endwhile
if lnrange[0] != -1
if has("folding") && foldclosed(line('.')) != -1
" try
" execute "normal! zO"
" catch /E490/ " no fold found
" endtry
execute "normal! zO"
endif
" let s:pythonsense_obj_start_line = -1
" let s:pythonsense_obj_end_line = -1
" execute "normal! \<ESC>gv"
endif
endfunction
function! pythonsense#python_function_text_object(inner, mode)
call pythonsense#python_text_object('def', a:inner, a:mode)
endfunction
function! pythonsense#python_class_text_object(inner, mode)
call pythonsense#python_text_object('class', a:inner, a:mode)
endfunction
" }}}1
" Python Docstring Text Objects {{{1
" From: https://pastebin.com/u/gfixler
function! pythonsense#python_docstring_text_object (inner)
" get current line number
let s = line('.')
" climb up to first def/class line, or first line of buffer
while s > 0 && getline(s) !~# '^\s*\(def\|async def\|class\)'
let s = s - 1
endwhile
" set search start to just after def/class line, or on first buffer line
let s = s + 1
" descend lines obj_end_line end of buffer or def/class line
while s < line('$') && getline(s) !~# '^\s*\(def\|async def\|class\)'
" if line begins with optional whitespace followed by '''
if getline(s) =~ "^\\s*'''" || getline(s) =~ '^\s*"""'
if getline(s) =~ "^\\s*'''"
let close_pattern = "'''\\s*$"
else
let close_pattern = '"""\s*$'
endif
" set search end to just after found start line
let e = s + 1
" descend lines obj_end_line end of buffer or def/class line
while e <= line('$') && getline(e) !~# '^\s*\(def\|async def\|class\)'
" if line ends with ''' followed by optional whitespace
if getline(e) =~ close_pattern
" TODO check first for blank lines above to select instead
" for 'around', extend search end through blank lines
if a:inner
let e -= 1
let s += 1
else
let x = e + 1
while x <= line('$') && getline(x) =~ '^\s*$'
let e = x
let x = x + 1
endwhile
endif
" visual line select from start to end (first cursor move)
exe 'norm '.s.'ggV'.e.'gg'
return
endif
" move search end down a line
let e = e + 1
endwhile
endif
" move search start down a line
let s = s + 1
endwhile
endfunction
" }}}1
" Python Movements {{{1
function! pythonsense#move_to_python_object(obj_name, to_end, fwd, vim_mode) range
if a:fwd
let initial_search_start_line = a:lastline
else
let initial_search_start_line = a:firstline
endif
if a:to_end
let target_line = pythonsense#find_end_of_python_object_to_move_to(a:obj_name, initial_search_start_line, a:fwd, v:count1)
else
let target_line = pythonsense#find_start_of_python_object_to_move_to(a:obj_name, initial_search_start_line, a:fwd, -1, v:count1)
endif
if target_line < 0 || target_line > line('$')
return
endif
let current_column = col('.')
let preserve_col_pos = get(b:, "pythonsense_preserve_col_pos", get(g:, "pythonsense_preserve_col_pos", 0))
let fold_open = ""
if a:vim_mode == "v"
normal! gv
endif
if has("folding") && foldclosed(line('.')) != -1
let fold_open = "zO"
else
let fold_open = ""
endif
try
if preserve_col_pos
execute "normal! " . target_line . "G" . current_column . "|" . preserve_col_pos . fold_open
else
execute "normal! " . target_line . "G^" . fold_open
endif
catch /E490/ " no fold found
endtry
endfunction
function! pythonsense#find_end_of_python_object_to_move_to(obj_name, start_line, fwd, nreps)
let initial_search_start_line = a:start_line
let effective_start_line = initial_search_start_line
let niters = 0
while niters < 2
let [start_line, nreps_remaining] = pythonsense#find_start_line_for_end_movement(a:obj_name, effective_start_line, a:fwd, a:nreps)
if start_line <= 0
let start_line = 1
endif
let start_of_object_line = pythonsense#find_start_of_python_object_to_move_to(a:obj_name, start_line, a:fwd, -1, nreps_remaining)
if start_of_object_line < 0 || start_of_object_line > line('$')
return -1
endif
let target_line = pythonsense#get_object_end_line_nr(start_of_object_line, start_of_object_line, 1)
if target_line > 0
\ && niters == 0
\ && (
\ target_line == initial_search_start_line
\ || (a:fwd && target_line < initial_search_start_line)
\ || (!a:fwd && target_line > initial_search_start_line)
\ )
" no change; possibly because we are already at an end boundary;
" make ONE more attempt at trying again
let niters += 1
if a:fwd
let effective_start_line = pythonsense#find_start_of_python_object_to_move_to(a:obj_name, target_line, a:fwd, -1, 1)
if effective_start_line <= 0
break
endif
else
let prev_obj_indent = pythonsense#get_line_indent_count(start_of_object_line)
let [new_start_line, nreps_remaining] = pythonsense#find_start_line_for_end_movement(a:obj_name, start_of_object_line, a:fwd, a:nreps)
while new_start_line > 0 && getline(new_start_line) =~ '^\s*$'
let new_start_line -= 1
endwhile
if new_start_line <= 0
break
endif
let start_of_object_line = pythonsense#find_start_of_python_object_to_move_to(a:obj_name, new_start_line, a:fwd, prev_obj_indent, nreps_remaining)
let target_line = pythonsense#get_object_end_line_nr(start_of_object_line, start_of_object_line, 1)
break
endif
else
break
endif
endwhile
if a:fwd && target_line < initial_search_start_line
return -1
elseif !a:fwd && target_line > initial_search_start_line
return -1
else
return target_line
endif
endfunction
function! pythonsense#find_start_of_python_object_to_move_to(obj_name, start_line, fwd, max_indent, nreps)
let current_line = a:start_line
if a:fwd
let stepvalue = 1
else
let stepvalue = -1
endif
let target_pattern = '^\s*' . a:obj_name . '\s\+'
let nreps_left = a:nreps
let start_line = current_line
let target_line = current_line
let scope_block_indent = a:max_indent
if getline(start_line) =~# target_pattern
let start_line += stepvalue
endif
while nreps_left > 0
while start_line > 0 && start_line <= line("$")
if getline(start_line) =~ '^\s*\(class\|def\|async def\)'
let current_line_indent = pythonsense#get_line_indent_count(start_line)
if getline(start_line) =~# target_pattern
if a:max_indent < 0 || current_line_indent < scope_block_indent
let target_line = start_line
break
endif
endif
if scope_block_indent == -1 || current_line_indent < scope_block_indent
let scope_block_indent = current_line_indent
endif
endif
let start_line += stepvalue
endwhile
if start_line < 1 || start_line > line("$")
break
endif
let start_line += stepvalue
let nreps_left -= 1
endwhile
return target_line
endfunction
function! pythonsense#find_start_line_for_end_movement(obj_name, initial_search_start_line, fwd, nreps_requested)
let start_line = a:initial_search_start_line
let nreps_remaining = a:nreps_requested
let target_pattern = '^\s*' . a:obj_name . '\s\+'
let scope_block_indent = -1
let is_found = 0
while start_line > 0
if getline(start_line) =~ '^\s*\(class\|def\|async def\)'
let current_line_indent = pythonsense#get_line_indent_count(start_line)
if getline(start_line) =~ target_pattern
if scope_block_indent == -1 || current_line_indent < scope_block_indent
let is_found = 1
break
endif
endif
if scope_block_indent == -1 || current_line_indent < scope_block_indent
let scope_block_indent = current_line_indent
endif
endif
let start_line -= 1
endwhile
if is_found
if !a:fwd
let start_line -= 1
else
let nreps_remaining -= 1 " skip finding this block
endif
endif
return [start_line, nreps_remaining]
endfunction
" }}}1
" Python Location Information {{{1
function! pythonsense#echo_python_location()
let indent_char = pythonsense#get_indent_char()
let pyloc = []
let current_line = line('.')
let obj_pattern = '\(class\|def\|async def\)'
while current_line > 0
if getline(current_line) !~ '^\s*$'
break
endif
let current_line = current_line - 1
endwhile
if current_line == 0
return
endif
let target_line_indent = pythonsense#get_line_indent_count(current_line)
if target_line_indent < 0
break
endif
let previous_line = current_line
while current_line > 0
let pattern = '^' . indent_char . '\{0,' . target_line_indent . '}' . obj_pattern
let current_line_text = getline(current_line)
if current_line_text =~# pattern
let obj_name = matchstr(current_line_text, '^\s*\(class\|def\|async def\)\s\+\zs\k\+')
if get(g:, "pythonsense_extended_location_info", 1)
let obj_type = matchstr(current_line_text, '^\s*\zs\(class\|def\|async def\)')
call add(pyloc, "(" . obj_type . ":)" . obj_name)
else
call add(pyloc, obj_name)
endif
let target_line_indent = pythonsense#get_line_indent_count(current_line) - 1
endif
if target_line_indent < 0
break
endif
let previous_line = current_line
let current_line = current_line - 1
endwhile
if get(g:, "pythonsense_extended_location_info", 1)
let joiner = " > "
else
let joiner = "."
endif
echo join(reverse(pyloc), joiner)
return
endfunction
" }}}1