" Insert Docstring. " Author: Shinya Ohyanagi " WebPage: http://github.com/heavenshell/vim-pydocstriong/ " Description: Generate Python docstring to your Python script file. " License: BSD, see LICENSE for more details. " NOTE: This module is heavily inspired by php-doc.vim and " sonictemplate.vim let s:save_cpo = &cpo set cpo&vim let g:pydocstring_templates_path = get(g:, 'pydocstring_templates_path', '') let g:pydocstring_formatter = get(g:, 'pydocstring_formatter', 'sphinx') let g:pydocstring_doq_path = get( \ g:, \ 'pydocstring_doq_path', \ printf('%s/lib/doq', expand(':p:h:h')) \ ) let g:pydocstring_ignore_init = get(g:, 'pydocstring_ignore_init', 0) let s:results = [] function! s:get_indent_width() abort " Get indentation width return &smarttab ? &shiftwidth : &softtabstop endfunction function! s:get_range() abort " Get visual mode selection. let mode = visualmode(1) if mode == 'v' || mode == 'V' || mode == '' let start_lineno = line("'<") let end_lineno = line("'>") return {'start_lineno': start_lineno, 'end_lineno': end_lineno} endif let current = line('.') return {'start_lineno': 0, 'end_lineno': '$'} endfunction function! s:insert_docstring(docstrings, end_lineno) abort let paste = &g:paste let &g:paste = 1 silent! execute 'normal! ' . a:end_lineno . 'G$' let current_lineno = line('.') " If current position is bottom, add docstring below. if a:end_lineno == current_lineno silent! execute 'normal! O' . a:docstrings['docstring'] else silent! execute 'normal! o' . a:docstrings['docstring'] endif let &g:paste = paste silent! execute 'normal! ' . a:end_lineno . 'G$' endfunction function! s:callback(msg, indent, start_lineno) abort let msg = join(a:msg, '') " Check needed for Neovim if len(msg) == 0 return endif let docstrings = reverse(json_decode(msg)) silent! execute 'normal! 0' let length = len(docstrings) for docstring in docstrings let lineno = 0 if length > 1 call cursor(a:start_lineno + docstring['start_lineno'] - 1, 1) let lineno = search('\:\(\s*#.*\)*$', 'n') + 1 else let lineno = search('\:\(\s*#.*\)*$', 'n') + 1 endif call s:insert_docstring(docstring, lineno) endfor endfunction function! s:format_callback(msg, indent, start_lineno) abort call extend(s:results, a:msg) endfunction function! s:exit_callback(msg) abort unlet s:job " Needed for Neovim let length = len(s:results) if length if length == 1 && s:results[0] == '' let s:results = [] return endif let view = winsaveview() silent execute '% delete' " Hack for Neovima PydocstringFormat " Neovim add blank line to the end of list if has('nvim') && s:results[-1] == '' call remove(s:results, -1) endif call setline(1, s:results) call winrestview(view) let s:results = [] endif endfunction function! s:execute(cmd, lines, indent, start_lineno, cb, ex_cb) abort if !executable(expand(g:pydocstring_doq_path)) redraw echohl Error echo '`doq` not found. Install `doq`.' echohl None return endif let s:results = [] if has('nvim') if exists('s:job') call jobstop(s:job) endif let s:job = jobstart(a:cmd, { \ 'on_stdout': {_c, m, _e -> a:cb(m, a:indent, a:start_lineno)}, \ 'on_exit': {_c, m, _e -> a:ex_cb(m)}, \ 'stdout_buffered': v:true, \ }) if exists('*chansend') " Neovim >0.3.0 call chansend(s:job, a:lines) call chanclose(s:job, 'stdin') else " Legacy API call jobsend(s:job, a:lines) call jobclose(s:job, 'stdin') endif else if exists('s:job') && job_status(s:job) != 'stop' call job_stop(s:job) endif let s:job = job_start(a:cmd, { \ 'callback': {_, m -> a:cb([m], a:indent, a:start_lineno)}, \ 'exit_cb': {_, m -> a:ex_cb([m])}, \ 'in_mode': 'nl', \ }) let channel = job_getchannel(s:job) if ch_status(channel) ==# 'open' call ch_sendraw(channel, a:lines) call ch_close_in(channel) endif endif endfunction function! s:create_cmd(style, omissions) abort if a:omissions ==# '' let cmd = printf( \ '%s --style=%s --formatter=%s --indent=%s', \ expand(g:pydocstring_doq_path), \ a:style, \ g:pydocstring_formatter, \ s:get_indent_width() \ ) else let cmd = printf( \ '%s --style=%s --formatter=%s --omit=%s --indent=%s', \ expand(g:pydocstring_doq_path), \ a:style, \ g:pydocstring_formatter, \ a:omissions, \ s:get_indent_width() \ ) endif if g:pydocstring_templates_path !=# '' let cmd = printf('%s --template_path=%s', cmd, expand(g:pydocstring_templates_path)) endif return cmd endfunction function! pydocstring#format() abort let lines = printf("%s\n", join(getbufline(bufnr('%'), 1, '$'), "\n")) let cmd = s:create_cmd('string', '') let cmd = g:pydocstring_ignore_init ? printf('%s --ignore_init', cmd) : cmd let indent = s:get_indent_width() let end_lineno = line('.') call s:execute( \ cmd, \ lines, \ indent, \ end_lineno, \ function('s:format_callback'), \ function('s:exit_callback') \ ) endfunction function! pydocstring#insert(...) abort let range = s:get_range() let pos = getpos('.') let line = getline('.') let indent = matchstr(line, '^\(\s*\)') let space = repeat(' ', s:get_indent_width()) let indent = indent . space if len(indent) == 0 let indent = space endif silent! execute 'normal! 0' let is_not_range = range['start_lineno'] == 0 && range['end_lineno'] == '$' if is_not_range let start_lineno = line('.') let end_lineno = search('\:\(\s*#.*\)*$') else let start_lineno = range['start_lineno'] let end_lineno = range['end_lineno'] endif call setpos('.', pos) let cmd = s:create_cmd('json', 'self,cls') let lines = join(getline(start_lineno, end_lineno), "\n") if is_not_range let lines = printf("%s\n%s%s", lines, indent, 'pass') let cmd = printf('%s --ignore_exception', cmd) endif call s:execute( \ cmd, \ lines, \ indent, \ start_lineno, \ function('s:callback'), \ function('s:exit_callback') \ ) endfunction let &cpo = s:save_cpo unlet s:save_cpo