diff --git a/README.rst b/README.rst index 26e556a..2318349 100644 --- a/README.rst +++ b/README.rst @@ -170,8 +170,10 @@ vim-ipython from defining any of the default mappings. By default, vim-ipython activates the custom completefunc globally. Sometimes, having a completefunc breaks other plugins' completions. Putting the line ``let g:ipy_completefunc = 'local'`` in one's vimrc will activate the -IPython-based completion only for current buffer. Setting `g:ipy_completefunc` -to anything other than `'local'` or `'global'` disables it altogether. +IPython-based completion only for current buffer. Putting ``let +g:ipy_completefunc = 'omni'`` will set the omnifunc option for the current +buffer. Setting `g:ipy_completefunc` to anything other than `'local'` or +`'global'` disables it altogether. **NEW since IPython 0.13** diff --git a/autoload/unite/sources/history_ipython.vim b/autoload/unite/sources/history_ipython.vim new file mode 100644 index 0000000..39edb0f --- /dev/null +++ b/autoload/unite/sources/history_ipython.vim @@ -0,0 +1,197 @@ +if exists('g:loaded_history_ipython') + finish +endif +let g:loaded_history_ipython = 1 + +let s:save_cpo = &cpo +set cpo&vim + +function! unite#sources#history_ipython#define() + return s:source +endfunction + +let s:source = { + \ 'name' : 'history/ipython', + \ 'description' : 'candidates from IPython history', + \ 'action_table' : {}, + \ 'hooks' : {}, + \ 'default_action' : 'send', + \ 'default_kind' : 'word', + \ 'syntax' : 'uniteSource__Python', + \ 'max_candidates' : 100, + \} + +function! s:source.hooks.on_syntax(args, context) + let save_current_syntax = get(b:, 'current_syntax', '') + unlet! b:current_syntax + + try + silent! syntax include @Python syntax/python.vim + syntax region uniteSource__IPythonPython + \ start=' ' end='$' contains=@Python containedin=uniteSource__IPython + let &l:iskeyword = substitute(&l:iskeyword, ',!$\|!,', '', '') + finally + let b:current_syntax = save_current_syntax + endtry +endfunction + +function! s:source.hooks.on_init(args, context) + if !exists('*IPythonHistory') + call unite#print_source_error( + \ 'IPythonHistory() does not exist', s:source.name) + return + endif + + let args = unite#helper#parse_source_args(a:args) + let a:context.source__session = get(a:context, 'source__session', -1) + if a:context.source__session == -1 + let a:context.source__session = get(args, 0, -1) + endif + let a:context.source__input = a:context.input + if a:context.source__input == '' || a:context.unite__is_restart + let a:context.source__input = unite#util#input('Pattern: ', + \ a:context.source__input, + \ 'customlist,IPythonCmdComplete') + endif + + call unite#print_source_message('Pattern: ' + \ . a:context.source__input, s:source.name) +endfunction + +function! s:source.gather_candidates(args, context) + if !exists('*IPythonHistory') + return [] + endif + + return map(IPythonHistory(a:context.source__input, + \ a:context.source__session), '{ + \ "word" : v:val.code, + \ "abbr" : printf("'''''' %d/%d '''''' %s", v:val.session, v:val.line, + \ v:val.code =~ "\n" ? + \ "\n" . join(split(v:val.code, "\n")[:50], "\n") : v:val.code), + \ "is_multiline" : 1, + \ "source__session" : v:val.session, + \ "source__line" : v:val.line, + \ "source__context" : a:context, + \ "action__regtype" : "V", + \ }') +endfunction + +let s:source.action_table.send = { + \ 'description' : 'run in IPython', + \ 'is_selectable' : 1, + \ } +function! s:source.action_table.send.func(candidates) + for candidate in a:candidates + let g:ipy_input = candidate.word + Python2or3 run_ipy_input() + silent! unlet g:ipy_input + endfor +endfunction + +let s:source.action_table.session = { + \ 'description' : "get history for candidate's session", + \ 'is_quit' : 0, + \ 'is_invalidate_cache' : 1, + \ } +function! s:source.action_table.session.func(candidate) + let context = a:candidate.source__context + let context.source__input = unite#util#input('Pattern: ', + \ context.source__input, + \ 'customlist,IPythonCmdComplete') + let context.source__session = a:candidate.source__session +endfunction + +let s:source.action_table.session_info = { + \ 'description' : "print information about a session", + \ 'is_quit' : 0, + \ } +function! s:source.action_table.session_info.func(candidate) + let store_history = get(g:, 'ipython_store_history', 1) + try + let g:ipython_store_history = 0 + let session_info = [ + \ "from IPython import get_ipython", + \ "def _session_info(session=0):", + \ " def date(d):", + \ " return d.strftime('%a %d%b%Y %T')", + \ " session_id, start, end, cmds, remark = " . + \ " get_ipython().history_manager.get_session_info(session)", + \ " val = 'start: {0}'.format(date(start))", + \ " if end:", + \ " val += '; end: {0}; {1} commands'.format(date(end), cmds)", + \ " return val", + \ ] + let g:ipy_input = join(session_info, "\n") + silent Python2or3 run_ipy_input(silent=True) + let g:ipy_input = printf('_session_info(%d)', a:candidate.source__session) + silent! unlet g:ipy_result + Python2or3 eval_ipy_input('g:ipy_result') + echomsg printf('session %d: %s', + \ a:candidate.source__session, g:ipy_result) + finally + let g:ipython_store_history = store_history + endtry +endfunction + +let s:source.action_table.macro = { + \ 'description' : 'create IPython macro', + \ 'is_selectable' : 1, + \ } +function! s:source.action_table.macro.func(candidates) + let g:ipy_input = printf('%%macro %s %s', + \ unite#util#input('Macro name: '), + \ join(map(a:candidates, + \ 'printf("%s/%s", v:val.source__session, v:val.source__line)')) + \ ) + Python2or3 run_ipy_input() + silent! unlet g:ipy_input +endfunction + +let s:source.action_table.yank = { + \ 'description' : 'yank candidates', + \ 'is_selectable' : 1, + \ 'is_quit' : 1, + \ } +function! s:source.action_table.yank.func(candidates) + if len(a:candidates) == 1 && a:candidates[0].word !~ "\n" + let text = a:candidates[0].word + let mode = 'v' + else + let text = join(map(copy(a:candidates), 'v:val.word'), "\n\n") + let mode = 'V' + endif + call setreg('"', text, mode) + if has('clipboard') + if &clipboard =~# '\' + call setreg('*', text, mode) + endif + if &clipboard =~# '\' + call setreg('+', text, mode) + endif + endif + + echohl Question | echo 'Yanked:' | echohl Normal + echo text +endfunction + +let s:source.action_table.append = { + \ 'description' : 'append candidates', + \ 'is_selectable' : 1, + \ } +function! s:source.action_table.append.func(candidates) + put = join(map(copy(a:candidates), 'v:val.word'), \"\n\n\") +endfunction + +let s:source.action_table.insert = { + \ 'description' : 'insert candidates', + \ 'is_selectable' : 1, + \ } +function! s:source.action_table.insert.func(candidates) + put! = join(map(copy(a:candidates), 'v:val.word'), \"\n\n\") +endfunction + +let &cpo = s:save_cpo +unlet s:save_cpo + +" vim:set et ts=2 sts=2 sw=2: diff --git a/doc/ipython.txt b/doc/ipython.txt new file mode 100644 index 0000000..a664ac1 --- /dev/null +++ b/doc/ipython.txt @@ -0,0 +1,159 @@ +*ipython.txt* + +============================================================================== +Contents *vim-ipython-contents* + +1. Completion Metadata |vim-ipython-metadata| +2. IPython Monitor |vim-ipython-monitor| +3. IPython History Unite Source |vim-ipython-history| +4. Variables |vim-ipython-variables| + 4.1. g:ipy_autostart |g:ipy_autostart| + 4.2. g:ipy_completefunc |g:ipy_completefunc| + 4.3. g:ipy_input |g:ipy_input| + 4.4. g:ipy_perform_mappings |g:ipy_perform_mappings| + 4.5. g:ipython_completion_timeout |g:ipython_completion_timeout| + 4.6. g:ipython_dictionary_completion |g:ipython_dictionary_completion| + 4.7. g:ipython_greedy_matching |g:ipython_greedy_matching| + 4.8. g:ipython_history_len |g:ipython_history_len| + 4.9. g:ipython_history_raw |g:ipython_history_raw| + 4.10. g:ipython_history_timeout |g:ipython_history_timeout| + 4.11. g:ipython_history_unique |g:ipython_history_unique| + 4.12. g:ipython_run_flags |g:ipython_run_flags| + 4.13. g:ipython_store_history |g:ipython_store_history| + 4.14. g:ipython_timeout |g:ipython_timeout| + +============================================================================== +1. Completion Metadata *vim-ipython-metadata* + +vim-ipython supports user-supplied metadata associated with completions from +the IPython shell. The plugin fetches the metadata from the IPython kernel +using the user-defined function `completion_metadata` which takes one +parameter - the result of `get_ipython()`. If the function does not exist in +IPython's namespace, completion will still work but without any menu/info +entries in the completion menu. Each completion match should have a +corresponding metadata dictionary with "word", "menu", and "info" fields. A +basic (and slow) implementation of such a function follows: > + + def completion_metadata(ip): + import inspect + import six + metadata = [dict(word=m) for m in ip.Completer.matches] + for m in metadata: + try: + obj = eval(m['word'], ip.user_ns) + except Exception: + continue + m['menu'] = six.moves.reprlib.repr(obj) + info = inspect.getdoc(obj) + if info: + m['info'] = info + return metadata + +============================================================================== +2. IPython Monitor *vim-ipython-monitor* + +The included `monitor.py` script listens in on message from Vim to the IPython +kernel to echo inputs and outputs to the kernel in real-time. The script must +be started before connecting Vim to the IPython kernel so that it can +differentiate between Vim and the Jupyter shell clients. The `:IPython` +command can be executed multiple times without ill effect in case the monitor +is started later on. + +Basic usage: > + + $ python monitor.py &; jupyter console + :IPython + +Note: Currently the script looks for a connection file automatically and will +connect to the first connection file it finds matching the glob pattern +"kernel-[0-9]*.json'. This means the script will not connect to IPython +notebook kernels by design. + +============================================================================== +3. IPython History Unite Source *vim-ipython-history* + +Note: Requires unite.vim: https://github.com/Shougo/unite.vim + +The plugin includes a Unite source named "history/ipython" providing an +interface to the history messaging in IPython. The source will prompt for a +glob pattern to search for. If no pattern is provided, the search results in +up to |g:ipython_history_len| of the most recent IPython commands. If the +pattern begins or ends with a '*', the other end of the pattern is anchored at +the start or end of the match. For example, > + + Pattern: def * + +will return results that start with a function definition and > + + Pattern: *) + +will return results ending with ')'. Otherwise a '*' is both prepended +and appended to the pattern, so > + + Pattern: sys + +will return results containing "sys" anywhere. + +The input prompt allows completion from the IPython namespace. + +After selecting a history entry, the available actions are (in addition to +Unite's common actions): + + - `append` (insert the entry after the cursor line) + - `insert` (insert the entry before the cursor line) + - `macro` (prompt for a macro name and create an IPython macro to repeat + the commands) + - `send` (repeat the command in IPython) + - `session` (restart the history/ipython source showing entries only from + the same session as the selected entry) + - `session_info` (print session date and start time) + - `yank` (yank entries to unnamed and clipboard if 'clipboard' is set) + +Multiple history entries may be selected for all of the actions. + +============================================================================== +4. Variables *vim-ipython-variables* + +------------------------------------------------------------------------------ +4.1. `g:ipy_autostart` *g:ipy_autostart* + +------------------------------------------------------------------------------ +4.2. `g:ipy_completefunc` *g:ipy_completefunc* + +------------------------------------------------------------------------------ +4.3. `g:ipy_input` *g:ipy_input* + +------------------------------------------------------------------------------ +4.4. `g:ipy_perform_mappings` *g:ipy_perform_mappings* + +------------------------------------------------------------------------------ +4.5. `g:ipython_completion_timeout` *g:ipython_completion_timeout* + +------------------------------------------------------------------------------ +4.6. `g:ipython_dictionary_completion` *g:ipython_dictionary_completion* + +------------------------------------------------------------------------------ +4.7. `g:ipython_greedy_matching` *g:ipython_greedy_matching* + +------------------------------------------------------------------------------ +4.8. `g:ipython_history_len` *g:ipython_history_len* + +------------------------------------------------------------------------------ +4.9. `g:ipython_history_raw` *g:ipython_history_raw* + +------------------------------------------------------------------------------ +4.10. `g:ipython_history_timeout` *g:ipython_history_timeout* + +------------------------------------------------------------------------------ +4.11. `g:ipython_history_unique` *g:ipython_history_unique* + +------------------------------------------------------------------------------ +4.12. `g:ipython_run_flags` *g:ipython_run_flags* + +------------------------------------------------------------------------------ +4.13. `g:ipython_store_history` *g:ipython_store_history* + +------------------------------------------------------------------------------ +4.14. `g:ipython_timeout` *g:ipython_timeout* + + vim: textwidth=78 et filetype=help:norightleft: diff --git a/ftplugin/python/ipy.vim b/ftplugin/python/ipy.vim index 8c7011b..d73da1e 100644 --- a/ftplugin/python/ipy.vim +++ b/ftplugin/python/ipy.vim @@ -18,42 +18,90 @@ " " written by Paul Ivanov (http://pirsquared.org) " -if !has('python') +if !(has('python') || has('python3')) " exit if python is not available. " XXX: raise an error message here finish endif +if has('python3') && get(g:, 'pymode_python', '') !=# 'python' + command! -nargs=1 Python2or3 python3 + Python2or3 PY3 = True + function! IPythonPyeval(arg) + return py3eval(a:arg) + endfunction +else + command! -nargs=1 Python2or3 python + Python2or3 PY3 = False + function! IPythonPyeval(arg) + return pyeval(a:arg) + endfunction +endif + " Allow custom mappings. if !exists('g:ipy_perform_mappings') let g:ipy_perform_mappings = 1 endif +if !exists('g:ipython_dictionary_completion') + let g:ipython_dictionary_completion = 0 +endif +if !exists('g:ipython_greedy_matching') + let g:ipython_greedy_matching = 0 +endif + +" Use -i with %run magic by default +if !exists('g:ipython_run_flags') + let g:ipython_run_flags = '-i' +endif + +" Automatically run :IPython in python files after running :IPython the first +" time +if !exists('g:ipy_autostart') + let g:ipy_autostart = 1 +endif + +if !exists('g:ipython_history_len') + let g:ipython_history_len = 100 +endif +if !exists('g:ipython_history_raw') + let g:ipython_history_raw = 1 +endif +if !exists('g:ipython_history_unique') + let g:ipython_history_unique = 1 +endif +if !exists('g:ipython_history_timeout') + let g:ipython_history_timeout = 2 +endif + " Register IPython completefunc " 'global' -- for all of vim (default). " 'local' -- only for the current buffer. +" 'omni' -- set omnifunc for current buffer. " otherwise -- don't register it at all. " " you can later set it using ':set completefunc=CompleteIPython', which will " correspond to the 'global' behavior, or with ':setl ...' to get the 'local' " behavior if !exists('g:ipy_completefunc') - let g:ipy_completefunc = 'global' + let g:ipy_completefunc = 'omni' endif -python << EOF +Python2or3 << endpython import vim import sys +import itertools as it +import operator as op vim_ipython_path = vim.eval("expand(':h')") sys.path.append(vim_ipython_path) from vim_ipython import * -EOF +endpython fun! toggle_send_on_save() if exists("s:ssos") && s:ssos == 0 let s:ssos = 1 - au BufWritePost *.py :py run_this_file() + au BufWritePost *.py :Python2or3 run_this_file() echo "Autosend On" else let s:ssos = 0 @@ -62,97 +110,139 @@ fun! toggle_send_on_save() endif endfun -" Update the vim-ipython shell when the cursor is not moving. -" You can change how quickly this happens after you stop moving the cursor by -" setting 'updatetime' (in milliseconds). For example, to have this event -" trigger after 1 second: -" -" :set updatetime 1000 -" -" NOTE: This will only be triggered once, after the first 'updatetime' -" milliseconds, *not* every 'updatetime' milliseconds. see :help CursorHold -" for more info. -" -" TODO: Make this easily configurable on the fly, so that an introspection -" buffer we may have opened up doesn't get closed just because of an idle -" event (i.e. user pressed \d and then left the buffer that popped up, but -" expects it to stay there). -au CursorHold *.*,vim-ipython :python if update_subchannel_msgs(): echo("vim-ipython shell updated (on idle)",'Operator') +augroup vim-ipython + autocmd! + au FileType python IPython + " Update the vim-ipython shell when the cursor is not moving. + " You can change how quickly this happens after you stop moving the cursor by + " setting 'updatetime' (in milliseconds). For example, to have this event + " trigger after 1 second: + " + " :set updatetime 1000 + " + " NOTE: This will only be triggered once, after the first 'updatetime' + " milliseconds, *not* every 'updatetime' milliseconds. see :help CursorHold + " for more info. + " + " TODO: Make this easily configurable on the fly, so that an introspection + " buffer we may have opened up doesn't get closed just because of an idle + " event (i.e. user pressed \d and then left the buffer that popped up, but + " expects it to stay there). + au CursorHold *.*,vim-ipython :Python2or3 if update_subchannel_msgs(): echo("vim-ipython shell updated (on idle)",'Operator') -" XXX: broken - cursor hold update for insert mode moves the cursor one -" character to the left of the last character (update_subchannel_msgs must be -" doing this) -"au CursorHoldI *.* :python if update_subchannel_msgs(): echo("vim-ipython shell updated (on idle)",'Operator') + " XXX: broken - cursor hold update for insert mode moves the cursor one + " character to the left of the last character (update_subchannel_msgs must be + " doing this) + "au CursorHoldI *.* :Python2or3 if update_subchannel_msgs(): echo("vim-ipython shell updated (on idle)",'Operator') -" Same as above, but on regaining window focus (mostly for GUIs) -au FocusGained *.*,vim-ipython :python if update_subchannel_msgs(): echo("vim-ipython shell updated (on input focus)",'Operator') + " Same as above, but on regaining window focus (mostly for GUIs) + au FocusGained *.*,vim-ipython :Python2or3 if update_subchannel_msgs(): echo("vim-ipython shell updated (on input focus)",'Operator') -" Update vim-ipython buffer when we move the cursor there. A message is only -" displayed if vim-ipython buffer has been updated. -au BufEnter vim-ipython :python if update_subchannel_msgs(): echo("vim-ipython shell updated (on buffer enter)",'Operator') + " Update vim-ipython buffer when we move the cursor there. A message is only + " displayed if vim-ipython buffer has been updated. + au BufEnter vim-ipython :Python2or3 if update_subchannel_msgs(): echo("vim-ipython shell updated (on buffer enter)",'Operator') +augroup END " Setup plugin mappings for the most common ways to interact with ipython. -noremap (IPython-RunFile) :python run_this_file() -noremap (IPython-RunLine) :python run_this_line() -noremap (IPython-RunLines) :python run_these_lines() -noremap (IPython-OpenPyDoc) :python get_doc_buffer() -noremap (IPython-UpdateShell) :python if update_subchannel_msgs(force=True): echo("vim-ipython shell updated",'Operator') -noremap (IPython-ToggleReselect) :python toggle_reselect() -"noremap (IPython-StartDebugging) :python send('%pdb') -"noremap (IPython-BreakpointSet) :python set_breakpoint() -"noremap (IPython-BreakpointClear) :python clear_breakpoint() -"noremap (IPython-DebugThisFile) :python run_this_file_pdb() -"noremap (IPython-BreakpointClearAll) :python clear_all_breaks() -noremap (IPython-ToggleSendOnSave) :call toggle_send_on_save() -noremap (IPython-PlotClearCurrent) :python run_command("plt.clf()") -noremap (IPython-PlotCloseAll) :python run_command("plt.close('all')") -noremap (IPython-RunLineAsTopLevel) :python dedent_run_this_line() -xnoremap (IPython-RunLinesAsTopLevel) :python dedent_run_these_lines() - -if g:ipy_perform_mappings != 0 - map (IPython-RunFile) - map (IPython-RunLine) - map (IPython-RunLines) - map d (IPython-OpenPyDoc) - map s (IPython-UpdateShell) - map (IPython-ToggleReselect) - "map (IPython-StartDebugging) - "map (IPython-BreakpointSet) - "map (IPython-BreakpointClear) - "map (IPython-DebugThisFile) - "map (IPython-BreakpointClearAll) - imap (IPython-RunFile) - imap (IPython-RunLines) - imap (IPython-RunFile) - map (IPython-ToggleSendOnSave) - "" Example of how to quickly clear the current plot with a keystroke - "map (IPython-PlotClearCurrent) - "" Example of how to quickly close all figures with a keystroke - "map (IPython-PlotCloseAll) - - "pi custom - map (IPython-RunFile) - map (IPython-RunLine) - imap (IPython-RunLine) - map (IPython-RunLineAsTopLevel) - xmap (IPython-RunLines) - xmap (IPython-RunLinesAsTopLevel) - - noremap I# - xnoremap I# - noremap :s/^\([ \t]*\)#/\1/ - xnoremap :s/^\([ \t]*\)#/\1/ +noremap (IPython-RunFile) :update:Python2or3 run_this_file() +noremap (IPython-ImportFile) :update:Python2or3 run_this_file('-n') +noremap (IPython-RunLine) :Python2or3 run_this_line() +if has('python3') && get(g:, 'pymode_python', '') !=# 'python' + noremap (IPython-RunLines) :python3 run_these_lines() + xnoremap (IPython-RunLinesAsTopLevel) :python3 dedent_run_these_lines() +else + noremap (IPython-RunLines) :python run_these_lines() + xnoremap (IPython-RunLinesAsTopLevel) :python dedent_run_these_lines() endif +noremap (IPython-OpenPyDoc) :Python2or3 get_doc_buffer() +noremap (IPython-UpdateShell) :Python2or3 if update_subchannel_msgs(force=True): echo("vim-ipython shell updated",'Operator') +noremap (IPython-ToggleReselect) :Python2or3 toggle_reselect() +"noremap (IPython-StartDebugging) :Python2or3 send('%pdb') +"noremap (IPython-BreakpointSet) :Python2or3 set_breakpoint() +"noremap (IPython-BreakpointClear) :Python2or3 clear_breakpoint() +"noremap (IPython-DebugThisFile) :Python2or3 run_this_file_pdb() +"noremap (IPython-BreakpointClearAll) :Python2or3 clear_all_breaks() +noremap (IPython-ToggleSendOnSave) :call toggle_send_on_save() +noremap (IPython-PlotClearCurrent) :Python2or3 run_command("plt.clf()") +noremap (IPython-PlotCloseAll) :Python2or3 run_command("plt.close('all')") +noremap (IPython-RunLineAsTopLevel) :Python2or3 dedent_run_this_line() +noremap (IPython-RunTextObj) :set opfunc=opfuncg@ +noremap (IPython-RunCell) :set opfunc=opfuncg@ap -command! -nargs=* IPython :py km_from_string("") -command! -nargs=0 IPythonClipboard :py km_from_string(vim.eval('@+')) -command! -nargs=0 IPythonXSelection :py km_from_string(vim.eval('@*')) -command! -nargs=* IPythonNew :py new_ipy("") -command! -nargs=* IPythonInterrupt :py interrupt_kernel_hack("") -command! -nargs=0 IPythonTerminate :py terminate_kernel_hack() +function! s:DoMappings() + let b:did_ipython = 1 + if g:ipy_perform_mappings != 0 + if &buftype == '' + map (IPython-RunFile) + map g (IPython-ImportFile) + endif + " map (IPython-RunLine) + map (IPython-RunTextObj) + map (IPython-RunLines) + "map ,d (IPython-OpenPyDoc) + map (IPython-UpdateShell) + map (IPython-ToggleReselect) + "map (IPython-StartDebugging) + "map (IPython-BreakpointSet) + "map (IPython-BreakpointClear) + "map (IPython-DebugThisFile) + "map (IPython-BreakpointClearAll) + imap (IPython-RunFile) + imap (IPython-RunLines) + " imap (IPython-RunFile) + map (IPython-ToggleSendOnSave) + "" Example of how to quickly clear the current plot with a keystroke + "map (IPython-PlotClearCurrent) + "" Example of how to quickly close all figures with a keystroke + "map (IPython-PlotCloseAll) + + "pi custom + map (IPython-RunFile) + " map x (IPython-RunLine) + " imap x (IPython-RunLine) + map (IPython-RunLineAsTopLevel) + "xmap x (IPython-RunLinesAsTopLevel) + xmap (IPython-RunLines) + map x (IPython-RunCell) + + " noremap I# + " xnoremap I# + " noremap :s/^\([ \t]*\)#/\1/ + " xnoremap :s/^\([ \t]*\)#/\1/ + + nnoremap :IPythonInterrupt + inoremap K :call GetDocBuffer() + endif + + augroup vim_ipython_autostart + autocmd! + autocmd BufEnter,BufNewFile *.py,--Python-- if g:ipy_autostart && !exists('b:did_ipython') + \ | call s:DoMappings() | endif + autocmd FileType python if g:ipy_autostart && !exists('b:did_ipython') + \ | call s:DoMappings() | endif + augroup END + + setlocal omnifunc=CompleteIPython +endfunction + +function! s:GetDocBuffer() + python get_doc_buffer() + nnoremap gi ZQ:undojoinstartinsert! + nnoremap q ZQ:undojoinstartinsert! + nnoremap ` p:if winheight(0)<30res 30endifundojoinstartinsert! +endfunction + +command! -nargs=* IPython :call DoMappings()|:Python2or3 km_from_string("") +command! -nargs=0 IPythonClipboard :Python2or3 km_from_string(vim.eval('@+')) +command! -nargs=0 IPythonXSelection :Python2or3 km_from_string(vim.eval('@*')) +command! -nargs=* IPythonNew :Python2or3 new_ipy("") +command! -nargs=* IPythonInterrupt :Python2or3 interrupt_kernel_hack("") +command! -nargs=0 IPythonTerminate :Python2or3 terminate_kernel_hack() +command! -nargs=0 -bang IPythonInput :Python2or3 InputPrompt(force='') +command! -nargs=0 -bang IPythonInputSecret :Python2or3 InputPrompt(force='', hide_input=True) function! IPythonBalloonExpr() -python << endpython +Python2or3 << endpython word = vim.eval('v:beval_text') reply = get_doc(word) vim.command("let l:doc = %s"% reply) @@ -160,48 +250,224 @@ endpython return l:doc endfunction +if g:ipython_greedy_matching + let s:split_pattern = "[^= \r\n*().@-]" +else + let s:split_pattern = '\k\|\.' +endif + +Python2or3 << endpython +def process_matches(matches, metadata, result): + if PY3: + completions = matches + else: + completions = [s.encode(vim_encoding) for s in matches] + if vim.vars['ipython_dictionary_completion'] and not vim.vars['ipython_greedy_matching']: + for char in '\'"': + if any(c.endswith(char + ']') for c in completions): + completions = [c for c in completions if c.endswith(char + ']')] + break + try: + completions, metadata = zip(*sorted(zip(completions, metadata), + key=lambda x: x[0].lstrip('%').lower())) + except ValueError: + pass + for c, m in zip(completions, metadata): + result.clear() + result['word'] = c + # vim can't handle null bytes in Python strings + for k, v in m.items(): + result[k] = v.replace('\0', '^@') + vim.command('call add(res, {%s})' % ','.join( + '"{k}": IPythonPyeval("r[\'{k}\']")'.format(k=k) + for k in result)) +endpython + fun! CompleteIPython(findstart, base) - if a:findstart + if a:findstart + " return immediately for imports + if getline('.')[:col('.')-1] =~# + \ '\v^\s*(from\s+\w+(\.\w+)*\s+import\s+(\w+,\s+)*|import\s+)' + let line = getline('.') + let s:start = col('.') - 1 + while s:start && line[s:start - 1] =~ '[._[:alnum:]]' + let s:start -= 1 + endwhile + Python2or3 current_line = vim.current.line + return s:start + endif " locate the start of the word - let line = getline('.') - let start = col('.') - 1 - while start > 0 && line[start-1] =~ '\k\|\.' "keyword - let start -= 1 + let line = split(getline('.')[:col('.')-1], '\zs') + let s:start = col('.') - 1 + if s:start == 0 || (len(line) == s:start && + \ line[s:start-2] !~ s:split_pattern && + \ !(g:ipython_greedy_matching && s:start >= 2 + \ && line[s:start-3] =~ '\k') && + \ join(line[s:start-3:s:start-2], '') !=# '].') + let s:start = -1 + return s:start + endif + let s:start = strchars(getline('.')[:col('.')-1]) - 1 + let bracket_level = 0 + while s:start > 0 && (line[s:start-1] =~ s:split_pattern + \ || (g:ipython_greedy_matching && line[s:start-1] == '.' + \ && s:start >= 2 && line[s:start-2] =~ '\k') + \ || (g:ipython_greedy_matching && line[s:start-1] == '-' + \ && s:start >= 2 && line[s:start-2] == '[') + \ || join(line[s:start-2:s:start-1], '') ==# '].') + if g:ipython_greedy_matching && line[s:start-1] == '[' + if (s:start == 1 || line[s:start-2] !~ '\k\|\]') + \ || bracket_level > 0 + break + endif + let bracket_level += 1 + elseif g:ipython_greedy_matching && line[s:start-1] == ']' + let bracket_level -= 1 + endif + let s:start -= 1 endwhile - echo start - python << endpython -current_line = vim.current.line -endpython - return start - else + Python2or3 current_line = vim.current.line + return s:start + len(join(line[: s:start], '')) - + \ len(getline('.')[: s:start]) + else " find months matching with "a:base" let res = [] - python << endpython + if s:start == -1 | return [] | endif + " don't complete numeric literals + if a:base =~? '\v^[-+]?\d*\.?\d+(e[-+]?\d+)?\.$' | return [] | endif + " don't complete incomplete string literals + if a:base =~? '\v^(([^''].*)?['']|([^"].*)?["])\.$' | return [] | endif + let start = s:start + Python2or3 << endpython base = vim.eval("a:base") -findstart = vim.eval("a:findstart") -matches = ipy_complete(base, current_line, vim.eval("col('.')")) -# we need to be careful with unicode, because we can have unicode -# completions for filenames (for the %run magic, for example). So the next -# line will fail on those: -#completions= [str(u) for u in matches] -# because str() won't work for non-ascii characters -# and we also have problems with unicode in vim, hence the following: -completions = [s.encode(vim_encoding) for s in matches] -## Additionally, we have no good way of communicating lists to vim, so we have -## to turn in into one long string, which can be problematic if e.g. the -## completions contain quotes. The next line will not work if some filenames -## contain quotes - but if that's the case, the user's just asking for -## it, right? -#completions = '["'+ '", "'.join(completions)+'"]' -#vim.command("let completions = %s" % completions) -## An alternative for the above, which will insert matches one at a time, so -## if there's a problem with turning a match into a string, it'll just not -## include the problematic match, instead of not including anything. There's a -## bit more indirection here, but I think it's worth it -for c in completions: - vim.command('call add(res,"'+c+'")') +try: + matches, metadata = ipy_complete(base, current_line, int(vim.eval('start')) + len(base)) +except IOError: + if vim.eval('exists("*jedi#completions")') == '1': + vim.command('setlocal omnifunc=jedi#completions') + else: + vim.command('setlocal omnifunc=') + vim.command('return []') +r = dict() # result object to let vim access namespace while in a function +process_matches(matches, metadata, r) endpython - "call extend(res,completions) return res - endif - endfun + endif +endfun + +function! IPythonHistory(pattern, ...) + let session = a:0 > 0 ? a:1 : (-1) + let res = [] + Python2or3 << endpython +n = vim.vars.get('ipython_history_len', 100) +pattern = vim.eval('a:pattern') +if pattern: + if not pattern.startswith('*') and not pattern.endswith('*'): + pattern = '*{0}*'.format(pattern) + pattern = pattern.replace('[', '[[]') +else: + pattern = None +unique = vim.eval('get(g:, "ipython_history_unique", "")') +unique = bool(int(unique)) if unique else pattern is not None +if int(vim.eval('session')) >= 0: + history = get_session_history(session=int(vim.eval('session')), + pattern=pattern) +else: + history = get_history(n, pattern=pattern, unique=unique) +seen = set() +for session, line, code in reversed( + [list(h)[-1] for _, h in it.groupby( + history, lambda i: (i[0], i[2]))]): + if not unique or code.strip() not in seen: + seen.add(code.strip()) + vim.command('call add(res, {' + '"session": +IPythonPyeval("session"), ' + '"line": +IPythonPyeval("line"), ' + '"code": IPythonPyeval("code")})') +endpython + return res +endfunction + +function! IPythonCmdComplete(arglead, cmdline, cursorpos, ...) + let res = [] +Python2or3 << endpython +arglead = vim.eval('a:arglead') +if ' ' in arglead and not (arglead.strip().startswith('from ') or + arglead.strip().startswith('import ')): + start = arglead.split()[-1] +else: + start = arglead + +try: + matches, metadata = ipy_complete(start, + vim.eval('a:cmdline'), + int(vim.eval('a:cursorpos'))) +except IOError: + vim.command('return []') + +if ' ' in arglead: + arglead = arglead.rpartition(' ')[0] + matches = ['%s %s' % (arglead, m) for m in matches] +if int(vim.eval('a:0')): + r = dict() + process_matches(matches, metadata, r) +endpython + if a:0 + return res + else + return IPythonPyeval('matches') + endif +endfunction + +function! GreedyCompleteIPython(findstart, base) + if a:findstart + let line = getline('.') + let start = col('.') - 1 + while start && line[start - 1] =~ '\S' + let start -= 1 + endwhile + return start + else + return IPythonCmdComplete(a:base, a:base, len(a:base), 1) + endif +endfunction + +function! s:opfunc(type) + " Originally from tpope/vim-scriptease + let sel_save = &selection + let cb_save = &clipboard + let reg_save = @@ + let left_save = getpos("'<") + let right_save = getpos("'>") + let vimode_save = visualmode() + try + set selection=inclusive clipboard-=unnamed clipboard-=unnamedplus + if a:type =~ '^\d\+$' + silent exe 'normal! ^v'.a:type.'$hy' + elseif a:type =~# '^.$' + silent exe "normal! `<" . a:type . "`>y" + elseif a:type ==# 'line' + silent exe "normal! '[V']y" + elseif a:type ==# 'block' + silent exe "normal! `[\`]y" + elseif a:type ==# 'visual' + silent exe "normal! gvy" + else + silent exe "normal! `[v`]y" + endif + redraw + let l:cmd = @@ + finally + let @@ = reg_save + let &selection = sel_save + let &clipboard = cb_save + exe "normal! " . vimode_save . "\" + call setpos("'<", left_save) + call setpos("'>", right_save) + endtry +Python2or3 << EOF +import textwrap +import vim +run_command(textwrap.dedent(vim.eval('l:cmd'))) +EOF +endfunction diff --git a/ftplugin/python/vim_ipython.py b/ftplugin/python/vim_ipython.py index 0a4ed08..0925760 100644 --- a/ftplugin/python/vim_ipython.py +++ b/ftplugin/python/vim_ipython.py @@ -1,11 +1,13 @@ -reselect = False # reselect lines after sending from Visual mode -show_execution_count = True # wait to get numbers for In[43]: feedback? -monitor_subchannel = True # update vim-ipython 'shell' on every send? -run_flags= "-i" # flags to for IPython's run magic when using +reselect = False # reselect lines after sending from Visual mode +show_execution_count = False # wait to get numbers for In[43]: feedback? +monitor_subchannel = False # update vim-ipython 'shell' on every send? current_line = '' +allow_stdin = True # whether or not to accept stdin requests +current_stdin_prompt = {} try: from queue import Empty # python3 convention + unicode = str except ImportError: from Queue import Empty @@ -18,7 +20,34 @@ def __getattribute__(self, key): vim = NoOp() print("uh oh, not running inside vim") +import ast +import os +import re import sys +import time +PY3 = sys.version_info[0] == 3 + +class VimVars(object): + + """Wrapper for vim.vars for converting bytes to str.""" + + def get(self, name, default=None): + var = vim.vars.get(name, default) + if PY3 and isinstance(var, bytes): + var = str(var, vim_encoding) + elif not PY3 and isinstance(var, str): + var = unicode(var, vim_encoding) + return var + + def __getitem__(self, name): + if name not in vim.vars: + raise KeyError(name) + return self.get(name) + + def __setitem__(self, name, value): + vim.vars[name] = value + +vim_vars = VimVars() # get around unicode problems when interfacing with vim vim_encoding=vim.eval('&encoding') or 'utf-8' @@ -41,7 +70,7 @@ def vim_variable(name, default=None): return vim.eval(name) if exists else default def vim_regex_escape(x): - for old, new in (("[", "\\["), ("]", "\\]"), (":", "\\:"), (".", "\."), ("*", "\\*")): + for old, new in (("[", "\\["), ("]", "\\]"), (":", "\\:"), (".", "\\."), ("*", "\\*")): x = x.replace(old, new) return x @@ -93,7 +122,10 @@ def new_ipy(s=''): new_ipy() """ - from IPython.kernel import KernelManager + try: + from jupyter_client import KernelManager + except ImportError: + from IPython.kernel import KernelManager km = KernelManager() km.start_kernel() return km_from_string(km.connection_file) @@ -107,90 +139,144 @@ def km_from_string(s=''): import IPython except ImportError: raise ImportError("Could not find IPython. " + _install_instructions) - from IPython.config.loader import KeyValueConfigLoader try: - from IPython.kernel import ( - KernelManager, - find_connection_file, - ) + from traitlets.config.loader import KeyValueConfigLoader + except ImportError: + from IPython.config.loader import KeyValueConfigLoader + try: + from jupyter_client import KernelManager, find_connection_file except ImportError: - # IPython < 1.0 - from IPython.zmq.blockingkernelmanager import BlockingKernelManager as KernelManager - from IPython.zmq.kernelapp import kernel_aliases try: - from IPython.lib.kernel import find_connection_file + from IPython.kernel import KernelManager, find_connection_file except ImportError: - # < 0.12, no find_connection_file - pass + # IPython < 1.0 + from IPython.zmq.blockingkernelmanager import BlockingKernelManager as KernelManager + from IPython.zmq.kernelapp import kernel_aliases + try: + from IPython.lib.kernel import find_connection_file + except ImportError: + # < 0.12, no find_connection_file + pass - global km, kc, send + global km, kc, send, history, complete, object_info + def try_find(s): + try: + return os.path.isfile(find_connection_file(s)) + except Exception: + return False + + # Test if connection is still alive + connected = False + starttime = time.time() + attempt = 0 s = s.replace('--existing', '') - if 'connection_file' in KernelManager.class_trait_names(): - # 0.12 uses files instead of a collection of ports - # include default IPython search path - # filefind also allows for absolute paths, in which case the search - # is ignored + while not connected and (time.time() - starttime) < 5.0: + if not attempt and os.path.isfile(s): + fullpath = s + elif not attempt and try_find(s): + fullpath = find_connection_file(s) + else: + try: + s = fullpath = find_connection_file('kernel*') + except IOError: + echo("IPython connection attempt #%d failed - no kernel file" % attempt, "Warning") + time.sleep(1) + continue + attempt += 1 + + if 'connection_file' in KernelManager.class_trait_names(): + # 0.12 uses files instead of a collection of ports + # include default IPython search path + # filefind also allows for absolute paths, in which case the search + # is ignored + try: + # XXX: the following approach will be brittle, depending on what + # connection strings will end up looking like in the future, and + # whether or not they are allowed to have spaces. I'll have to sync + # up with the IPython team to address these issues -pi + if '--profile' in s: + k,p = s.split('--profile') + k = k.lstrip().rstrip() # kernel part of the string + p = p.lstrip().rstrip() # profile part of the string + fullpath = find_connection_file(k,p) + else: + fullpath = find_connection_file(s.lstrip().rstrip()) + except IOError as e: + echo(":IPython " + s + " failed", "Info") + echo("^-- failed '" + s + "' not found", "Error") + return + km = KernelManager(connection_file = fullpath) + km.load_connection_file() + else: + if s == '': + echo(":IPython 0.11 requires the full connection string") + return + loader = KeyValueConfigLoader(s.split(), aliases=kernel_aliases) + cfg = loader.load_config()['KernelApp'] + try: + km = KernelManager( + shell_address=(ip, cfg['shell_port']), + sub_address=(ip, cfg['iopub_port']), + stdin_address=(ip, cfg['stdin_port']), + hb_address=(ip, cfg['hb_port'])) + except KeyError as e: + echo(":IPython " +s + " failed", "Info") + echo("^-- failed --"+e.message.replace('_port','')+" not specified", "Error") + return + try: - # XXX: the following approach will be brittle, depending on what - # connection strings will end up looking like in the future, and - # whether or not they are allowed to have spaces. I'll have to sync - # up with the IPython team to address these issues -pi - if '--profile' in s: - k,p = s.split('--profile') - k = k.lstrip().rstrip() # kernel part of the string - p = p.lstrip().rstrip() # profile part of the string - fullpath = find_connection_file(k,p) - else: - fullpath = find_connection_file(s.lstrip().rstrip()) - except IOError as e: - echo(":IPython " + s + " failed", "Info") - echo("^-- failed '" + s + "' not found", "Error") - return - km = KernelManager(connection_file = fullpath) - km.load_connection_file() - else: - if s == '': - echo(":IPython 0.11 requires the full connection string") - return - loader = KeyValueConfigLoader(s.split(), aliases=kernel_aliases) - cfg = loader.load_config()['KernelApp'] + kc = km.client() + except AttributeError: + # 0.13 + kc = km + kc.start_channels() + + execute = kc.execute if hasattr(kc, 'execute') else kc.shell_channel.execute + history = kc.history if hasattr(kc, 'history') else kc.shell_channel.history + complete = kc.complete if hasattr(kc, 'complete') else kc.shell_channel.complete + object_info = kc.inspect if hasattr(kc, 'inspect') else kc.shell_channel.object_info + + def send(msg, **kwargs): + kwds = dict( + store_history=vim_vars.get('ipython_store_history', True), + allow_stdin=allow_stdin, + ) + kwds.update(kwargs) + return execute(msg, **kwds) + + send('', silent=True) try: - km = KernelManager( - shell_address=(ip, cfg['shell_port']), - sub_address=(ip, cfg['iopub_port']), - stdin_address=(ip, cfg['stdin_port']), - hb_address=(ip, cfg['hb_port'])) - except KeyError as e: - echo(":IPython " +s + " failed", "Info") - echo("^-- failed --"+e.message.replace('_port','')+" not specified", "Error") - return + msg = kc.shell_channel.get_msg(timeout=1) + connected = True + except: + echo("IPython connection attempt #%d failed - no messages" % attempt, "Warning") + kc.stop_channels() + continue - try: - kc = km.client() - except AttributeError: - # 0.13 - kc = km - kc.start_channels() + #XXX: backwards compatibility for IPython < 1.0 + if not hasattr(kc, 'iopub_channel'): + kc.iopub_channel = kc.sub_channel + set_pid() - try: - send = kc.execute - except AttributeError: - # < 3.0 - send = kc.shell_channel.execute + if not connected: + echo("IPython connection attempt timed out", "Error") + return + else: + vim.command('redraw') + echo("IPython connection successful") + send('"_vim_client";_=_;__=__', store_history=False) #XXX: backwards compatibility for IPython < 0.13 - try: + sc = kc.shell_channel + if hasattr(sc, 'object_info'): import inspect - sc = kc.shell_channel num_oinfo_args = len(inspect.getargspec(sc.object_info).args) if num_oinfo_args == 2: # patch the object_info method which used to only take one argument klass = sc.__class__ klass._oinfo_orig = klass.object_info klass.object_info = lambda s,x,y: s._oinfo_orig(x) - except: - pass #XXX: backwards compatibility for IPython < 1.0 if not hasattr(kc, 'iopub_channel'): @@ -205,6 +291,8 @@ def km_from_string(s=''): set completefunc=CompleteIPython elseif g:ipy_completefunc == 'local' setl completefunc=CompleteIPython + elseif g:ipy_completefunc == 'omni' + setl omnifunc=CompleteIPython endif """) # also activate GUI doc balloons if in gvim @@ -213,7 +301,6 @@ def km_from_string(s=''): set bexpr=IPythonBalloonExpr() endif """) - set_pid() return km def echo(arg,style="Question"): @@ -232,25 +319,63 @@ def disconnect(): def get_doc(word, level=0): if kc is None: return ["Not connected to IPython, cannot query: %s" % word] - msg_id = kc.shell_channel.object_info(word, level) + if word.startswith('%'): # request for magic documentation + request = ('_doc = get_ipython().object_inspect("{0}", ' + 'detail_level={1})\n' + 'del _doc["argspec"]').format(word, level) + try: + msg_id = send(request, silent=True, user_variables=['_doc']) + except TypeError: # change in IPython 3.0+ + msg_id = send(request, silent=True, user_expressions={'_doc':'_doc'}) + else: + msg_id = object_info(word, detail_level=level) doc = get_doc_msg(msg_id) # get around unicode problems when interfacing with vim return [d.encode(vim_encoding) for d in doc] -import re -# from http://serverfault.com/questions/71285/in-centos-4-4-how-can-i-strip-escape-sequences-from-a-text-file -strip = re.compile('\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]') +def strip_ansi_colour(text): + """Strip ANSI colour sequences from a string. + + Args: + text (str): Text string to be stripped. + + Returns: + iter[str]: A generator for each returned character. Note, + this will include newline characters. + + """ + import io + buff = io.StringIO(text) + while (b := buff.read(1)): + if b == '\x1b': + while (b := buff.read(1)) != 'm': + continue + else: + yield b + def strip_color_escapes(s): - return strip.sub('',s) + return ''.join(strip_ansi_colour(s)) def get_doc_msg(msg_id): n = 13 # longest field name (empirically) b=[] try: - content = get_child_msg(msg_id)['content'] + m = get_child_msg(msg_id) except Empty: # timeout occurred return ["no reply from IPython kernel"] + content = m['content'] + + if 'evalue' in content: + return b + + doc = None + if 'user_variables' in content: + doc = content['user_variables']['_doc'] + elif 'user_expressions' in content: + doc = content['user_expressions']['_doc'] + if doc: + content = ast.literal_eval(doc['data']['text/plain']) if not content['found']: return b @@ -261,6 +386,9 @@ def get_doc_msg(msg_id): text = content['data']['text/plain'] for line in text.split('\n'): b.append(strip_color_escapes(line).rstrip()) + if 'signature: ' in b[-1].lower() and b[-1].endswith(')'): + left, _, right = b[-1].partition(': ') + b[-1] = '{0}: `{1}`'.format(left, right) return b except KeyError: # no text/plain key return b @@ -270,7 +398,7 @@ def get_doc_msg(msg_id): c = content.get(field,None) if c: if field in ['definition']: - c = strip_color_escapes(c).rstrip() + c = '`%s`' % strip_color_escapes(c).rstrip() s = field.replace('_',' ').title()+':' s = s.ljust(n) if c.find('\n')==-1: @@ -280,30 +408,34 @@ def get_doc_msg(msg_id): b.extend(c.splitlines()) return b -def get_doc_buffer(level=0): +def get_doc_buffer(level=0, word=None): # empty string in case vim.eval return None vim.command("let isk_save = &isk") # save iskeyword list vim.command("let &isk = '@,48-57,_,192-255,.'") - word = vim.eval('expand("")') or '' + word = word or vim.eval('expand("")') vim.command("let &isk = isk_save") # restore iskeyword list doc = get_doc(word, level) if len(doc) ==0: echo(repr(word)+" not found","Error") return # documentation buffer name is same as the query made to ipython - vim.command('new '+word) + vim.command('new '+word.lstrip('%')) vim.command('setlocal modifiable noro') # doc window quick quit keys: 'q' and 'escape' vim.command('nnoremap q :q') + # shortcuts to change filetype/syntax + vim.command('nnoremap m :setfiletype man') + vim.command('nnoremap p :setfiletype python') + vim.command('nnoremap r :setfiletype rst') # Known issue: to enable the use of arrow keys inside the terminal when # viewing the documentation, comment out the next line - vim.command('nnoremap :q') + # vim.command('nnoremap :q') # and uncomment this line (which will work if you have a timoutlen set) #vim.command('nnoremap :q') b = vim.current.buffer b[:] = None b[:] = doc - vim.command('setlocal nomodified bufhidden=wipe') + vim.command('setlocal nomodified bufhidden=wipe nomodifiable readonly nospell') #vim.command('setlocal previewwindow nomodifiable nomodified ro') #vim.command('set previewheight=%d'%len(b))# go to previous window vim.command('resize %d'%len(b)) @@ -311,31 +443,80 @@ def get_doc_buffer(level=0): #vim.command('pedit doc') #vim.command('normal! ') # go to previous window if level == 0: - # use the ReST formatting that ships with stock vim - vim.command('setlocal syntax=rst') + # highlight python code within rst + vim.command(r'unlet! b:current_syntax') + vim.command(r'syn include @rstPythonScript syntax/python.vim') + # 4 spaces + vim.command(r'syn region rstPythonRegion start=/^\v {4}/ end=/\v^( {4}|\n)@!/ contains=@rstPythonScript') + # >>> python code -> (doctests) + vim.command(r'syn region rstPythonRegion matchgroup=pythonDoctest start=/^>>>\s*/ end=/\n/ contains=@rstPythonScript') + vim.command(r'set syntax=rst') else: # use Python syntax highlighting vim.command('setlocal syntax=python') def ipy_complete(base, current_line, pos): - # pos is the location of the start of base, add the length - # to get the completion position - msg_id = kc.shell_channel.complete(base, current_line, - int(pos) + len(base) - 1) + if re.match(r'^\s*(import|from)\s+', current_line): + pos -= len(current_line) - len(current_line.lstrip()) + current_line = current_line.lstrip() + else: + match = re.match(r'^\s*from\s+\w+(\.\w+)*\s+import\s+(\w+,\s+)*', current_line) + if match: + module = match.string.strip().split()[1] + current_line = 'from {module} import {base}'.format( + module=module, base=base) + pos = current_line.rindex(base) + else: + current_line = current_line[pos-len(base):pos] + pos = len(base) try: - m = get_child_msg(msg_id) - matches = m['content']['matches'] - matches.insert(0,base) # the "no completion" version - # we need to be careful with unicode, because we can have unicode - # completions for filenames (for the %run magic, for example). So the next - # line will fail on those: - #completions= [str(u) for u in matches] - # because str() won't work for non-ascii characters - # and we also have problems with unicode in vim, hence the following: - return matches + msg_id = complete(text=base, line=current_line, cursor_pos=pos) + except TypeError: + msg_id = complete(code=current_line, cursor_pos=pos) + try: + m = get_child_msg(msg_id, timeout=vim_vars.get('ipython_completion_timeout', 2)) + try: + return get_completion_metadata(base, m['content']) + except KeyError: # completion_metadata function not available + matches = m['content']['matches'] + metadata = [{} for _ in matches] + return matches, metadata except Empty: echo("no reply from IPython kernel") - return [''] + raise IOError + +def get_completion_metadata(base, content): + """Generate and fetch completion metadata.""" + request = f''' +try: + _completion_args = ( + get_ipython(), + {content['matches']!r}, + {base!r}, + {content['cursor_start']!r}, + {content['cursor_end']!r}, + ) + _completions = completion_metadata(*_completion_args) +except Exception: + pass +''' + try: + msg_id = send(request, silent=True, user_variables=['_completions']) + except TypeError: # change in IPython 3.0+ + msg_id = send(request, silent=True, user_expressions={'_completions':'_completions'}) + try: + m = get_child_msg(msg_id, timeout=vim_vars.get('ipython_completion_timeout', 2)) + except Empty: + echo("no reply from IPython kernel") + raise IOError + content = m['content'] + if 'user_variables' in content: + metadata = content['user_variables']['_completions'] + else: + metadata = content['user_expressions']['_completions'] + metadata = ast.literal_eval(metadata['data']['text/plain']) + matches = [c['word'] for c in metadata] + return matches, metadata def vim_ipython_is_open(): """ @@ -356,15 +537,22 @@ def update_subchannel_msgs(debug=False, force=False): if kc is None or (not vim_ipython_is_open() and not force): return False msgs = kc.iopub_channel.get_msgs() + if allow_stdin: + msgs += kc.stdin_channel.get_msgs() + + global current_stdin_prompt b = vim.current.buffer startedin_vimipython = vim.eval('@%')=='vim-ipython' + nwindows = len(vim.windows) + currentwin = int(vim.eval('winnr()')) + previouswin = int(vim.eval('winnr("#")')) if not startedin_vimipython: # switch to preview window vim.command( "try" "|silent! wincmd P" - "|catch /^Vim\%((\a\+)\)\=:E441/" - "|silent pedit +set\ ma vim-ipython" + "|catch /^Vim\\%((\\a\\+)\\)\\=:E441/" + "|silent pedit +set\\ ma vim-ipython" "|silent! wincmd P" "|endtry") # if the current window is called 'vim-ipython' @@ -374,7 +562,7 @@ def update_subchannel_msgs(debug=False, force=False): else: # close preview window, it was something other than 'vim-ipython' vim.command("pcl") - vim.command("silent pedit +set\ ma vim-ipython") + vim.command("silent pedit +set\\ ma vim-ipython") vim.command("wincmd P") #switch to preview window # subchannel window quick quit key 'q' vim.command('nnoremap q :q') @@ -407,6 +595,9 @@ def update_subchannel_msgs(debug=False, force=False): b = vim.current.buffer update_occured = False for m in msgs: + # if we received a message it means the kernel is not waiting for input + vim.command('autocmd! InsertEnter ') + current_stdin_prompt.clear() s = '' if 'msg_type' not in m['header']: # debug information @@ -445,7 +636,12 @@ def update_subchannel_msgs(debug=False, force=False): elif header == 'pyerr' or header == 'error': c = m['content'] s = "\n".join(map(strip_color_escapes,c['traceback'])) - s += c['ename'] + ":" + c['evalue'] + elif header == 'input_request': + current_stdin_prompt['prompt'] = m['content']['prompt'] + current_stdin_prompt['is_password'] = m['content']['password'] + current_stdin_prompt['parent_msg_id'] = m['parent_header']['msg_id'] + s += m['content']['prompt'] + echo('Awaiting input. call :IPythonInput to reply') if s.find('\n') == -1: # somewhat ugly unicode workaround from @@ -460,25 +656,38 @@ def update_subchannel_msgs(debug=False, force=False): b.append([l.encode(vim_encoding) for l in s.splitlines()]) update_occured = True # make a newline so we can just start typing there - if status_blank_lines: + if status_blank_lines and not current_stdin_prompt: if b[-1] != '': b.append(['']) if update_occured or force: vim.command('normal! G') # go to the end of the file - if not startedin_vimipython: - vim.command('normal! p') # go back to where you were + if current_stdin_prompt: + vim.command('normal! $') # also go to the end of the line + + if len(vim.windows) > nwindows: + pwin = int(vim.current.window.number) + if pwin <= previouswin: + previouswin += 1 + if pwin <= currentwin: + currentwin += 1 + vim.command(str(previouswin) + 'wincmd w') + vim.command(str(currentwin) + 'wincmd w') return update_occured -def get_child_msg(msg_id): +def get_child_msg(msg_id, timeout=None): # XXX: message handling should be split into its own process in the future + if timeout is None: + timeout = float(vim_vars.get('ipython_timeout', 1)) while True: # get_msg will raise with Empty exception if no messages arrive in 1 second - m = kc.shell_channel.get_msg(timeout=1) + m = kc.shell_channel.get_msg(timeout=timeout) if m['parent_header']['msg_id'] == msg_id: break else: #got a message, but not the one we were looking for - echo('skipping a message on shell_channel','WarningMsg') + if m['msg_type'] != 'execute_reply': + echo('skipping a message on shell_channel (%s)' % m['msg_type'], + 'WarningMsg') return m def print_prompt(prompt,msg_id=None): @@ -491,15 +700,17 @@ def print_prompt(prompt,msg_id=None): count = child['content']['execution_count'] echo("In[%d]: %s" %(count,prompt)) except Empty: - echo("In[]: %s (no reply from IPython kernel)" % prompt) + # if the kernel it's waiting for input it's normal to get no reply + if not kc.stdin_channel.msg_ready(): + echo("In[]: %s (no reply from IPython kernel)" % prompt) else: echo("In[]: %s" % prompt) -def with_subchannel(f,*args): +def with_subchannel(f,*args,**kwargs): "conditionally monitor subchannel" - def f_with_update(*args): + def f_with_update(*args, **kwargs): try: - f(*args) + f(*args,**kwargs) if monitor_subchannel: update_subchannel_msgs(force=True) except AttributeError: #if kc is None @@ -507,9 +718,28 @@ def f_with_update(*args): return f_with_update @with_subchannel -def run_this_file(): - msg_id = send('%%run %s %s' % (run_flags, repr(vim.current.buffer.name),)) - print_prompt("In[]: %%run %s %s" % (run_flags, repr(vim.current.buffer.name)),msg_id) +def run_this_file(flags=''): + ext = os.path.splitext(vim.current.buffer.name)[-1][1:] + if ext in ('pxd', 'pxi', 'pyx', 'pyxbld'): + cmd = ' '.join(filter(None, ( + '%run_cython', + vim_vars.get('cython_run_flags', ''), + repr(vim.current.buffer.name)))) + else: + cmd = '%%run %s %s' % (flags or vim_vars['ipython_run_flags'], + repr(vim.current.buffer.name)) + msg_id = send(cmd) + print_prompt(cmd, msg_id) + +@with_subchannel +def run_ipy_input(**kwargs): + lines = vim_vars['ipy_input'] + if lines.strip().endswith('?'): + return get_doc_buffer(level=1 if lines.strip().endswith('??') else 0, + word=lines.strip().rstrip('?')) + msg_id = send(lines, **kwargs) + lines = lines.replace('\n', u'\xac') + print_prompt(lines[:(int(vim.options['columns']) - 22)], msg_id) @with_subchannel def run_this_line(dedent=False): @@ -570,13 +800,83 @@ def run_these_lines(dedent=False): prompt = "lines %d-%d "% (r.start+1,r.end+1) print_prompt(prompt,msg_id) +@with_subchannel +def InputPrompt(force=False, hide_input=False): + msgs = kc.stdin_channel.get_msgs() + for m in msgs: + global current_stdin_prompt + if 'msg_type' not in m['header']: + continue + current_stdin_prompt.clear() + header = m['header']['msg_type'] + if header == 'input_request': + current_stdin_prompt['prompt'] = m['content']['prompt'] + current_stdin_prompt['is_password'] = m['content']['password'] + current_stdin_prompt['parent_msg_id'] = m['parent_header']['msg_id'] + + if not hide_input: + hide_input = current_stdin_prompt.get('is_password', False) + # If there is a pending input or we are forcing the input prompt + if (current_stdin_prompt or force) and kc: + # save the current prompt, ask for input and restore the prompt + vim.command('call inputsave()') + input_call = ( + "try" + "|let user_input = {input_command}('{prompt}')" + "|catch /^Vim:Interrupt$/" + "|silent! unlet user_input" + "|endtry" + ).format(input_command='inputsecret' if hide_input else 'input', + prompt=current_stdin_prompt.get('prompt', '')) + vim.command(input_call) + vim.command('call inputrestore()') + + # if the user replied to the input request + if vim.eval('exists("user_input")'): + reply = vim.eval('user_input') + vim.command("silent! unlet user_input") + # write the reply to the vim-ipython buffer if it's not a password + if not hide_input and vim_ipython_is_open(): + + currentwin = int(vim.eval('winnr()')) + previouswin = int(vim.eval('winnr("#")')) + vim.command( + "try" + "|silent! wincmd P" + "|catch /^Vim\\%((\\a\\+)\\)\\=:E441/" + "|endtry") + + if vim.eval('@%')=='vim-ipython': + b = vim.current.buffer + last_line = b[-1] + del b[-1] + b.append((last_line+reply).splitlines()) + vim.command(str(previouswin) + 'wincmd w') + vim.command(str(currentwin) + 'wincmd w') + + kc.input(reply) + if current_stdin_prompt: + try: + child = get_child_msg(current_stdin_prompt['parent_msg_id']) + except Empty: + pass + + current_stdin_prompt.clear() + return True + else: + if not current_stdin_prompt: + echo('no input request detected') + if not kc: + echo('not connected to IPython') + return False + def set_pid(): """ Explicitly ask the ipython kernel for its pid """ global pid - lines = '\n'.join(['import os', '_pid = os.getpid()']) + lines = '\n'.join(['import os as _os', '_pid = _os.getpid()']) try: msg_id = send(lines, silent=True, user_variables=['_pid']) @@ -601,6 +901,56 @@ def set_pid(): return pid +def eval_ipy_input(var=None): + ipy_input = vim_vars['ipy_input'] + if not ipy_input: + return + if ipy_input.startswith(('%', '!', '$')): + msg_id = send('', silent=True, + user_expressions={'_expr': ipy_input}) + else: + msg_id = send('from __future__ import division; ' + '_expr = %s' % ipy_input, silent=True, + user_expressions={'_expr': '_expr'}) + try: + child = get_child_msg(msg_id) + except Empty: + echo("no reply from IPython kernel") + return + result = child['content']['user_expressions'] + try: + text = result['_expr']['data']['text/plain'] + if not PY3 and isinstance(text, str): + text = unicode(text, vim_encoding) + if var: + try: + from cStringIO import StringIO + except ImportError: + from io import StringIO + from tokenize import STRING, generate_tokens + if next(generate_tokens(StringIO(text).readline))[0] == STRING: + from ast import parse + vim_vars[var.replace('g:', '')] = parse(text).body[0].value.s + else: + vim.command('let %s = "%s"' % ( + var, text.replace('\\', '\\\\').replace('"', '\\"'))) + else: + vim.command('call setreg(\'"\', "%s")' % + text.replace('\\', '\\\\').replace('"', '\\"')) + except KeyError: + try: + try: + echo('{ename}: {evalue}'.format(**child['content'])) + except KeyError: + echo('{ename}: {evalue}'.format(**result['_expr'])) + except Exception: + echo('Unknown error occurred') + else: + if not var: + vim.command('let @+ = @"') + vim.command('let @* = @"') + + def terminate_kernel_hack(): "Send SIGTERM to our the IPython kernel" import signal @@ -674,3 +1024,45 @@ def toggle_reselect(): # #send('run -d %s' % (vim.current.buffer.name,)) # echo("In[]: run -d %s (using pdb)" % vim.current.buffer.name) +def get_history(n, pattern=None, unique=True): + msg_id = history( + hist_access_type='search' if pattern else 'tail', + pattern=pattern, n=n, unique=unique, + raw=vim_vars.get('ipython_history_raw', True)) + try: + child = get_child_msg( + msg_id, timeout=float(vim_vars.get('ipython_history_timeout', 2))) + results = [(session, line, code.encode(vim_encoding)) + for session, line, code in child['content']['history']] + except Empty: + echo("no reply from IPython kernel") + return [] + if unique: + results.extend(get_session_history(pattern=pattern)) + return results + +def get_session_history(session=None, pattern=None): + from ast import literal_eval + from fnmatch import fnmatch + msg_id = send('', silent=True, user_expressions={ + '_hist': '[h for h in get_ipython().history_manager.get_range(' + '%s, raw=%s)]' + % (str(session) if session else + 'get_ipython().history_manager.session_number', + vim_vars.get('ipython_history_raw', 'True')), + '_session': 'get_ipython().history_manager.session_number', + }) + try: + child = get_child_msg( + msg_id, timeout=float(vim_vars.get('ipython_history_timeout', 2))) + hist = child['content']['user_expressions']['_hist'] + session = child['content']['user_expressions']['_session'] + session = int(session['data']['text/plain'].encode(vim_encoding)) + hist = literal_eval(hist['data']['text/plain']) + return [(s if s > 0 else session, l, c.encode(vim_encoding)) + for s, l, c in hist if fnmatch(c, pattern or '*')] + except Empty: + echo("no reply from IPython kernel") + return [] + except KeyError: + return [] diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..781c9db --- /dev/null +++ b/monitor.py @@ -0,0 +1,222 @@ +""" +Monitor for IPython/Jupyter console commands run from Vim. + +Usage: + 1. Run jupyter/ipython console + 2. Run python monitor.py + 3. Connect Vim to console kernel using IPython command +""" +from __future__ import print_function +import ast +import os +import re +import six +import sys +try: + from jupyter_client import KernelManager, find_connection_file +except ImportError: + from IPython.kernel import KernelManager, find_connection_file +try: + from Queue import Empty +except ImportError: + from queue import Empty +from glob import glob + +try: + from pygments import highlight +except ImportError: + highlight = lambda code, *args: code +else: + from pygments.lexers import PythonLexer, Python3Lexer + from pygments.formatters import TerminalFormatter + formatter = TerminalFormatter() + lexer = Python3Lexer() if six.PY3 else PythonLexer() + +colors = {k: i for i, k in enumerate([ + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'])} + + +def paths(): + for fullpath in glob(os.path.join(os.path.dirname(filename), 'kernel*')): + if not re.match('^(.*/)?kernel-[0-9]+.json', fullpath): + continue + yield fullpath + + +connected = False +while not connected: + try: + filename = find_connection_file('kernel*') + except IOError: + continue + + for fullpath in paths(): + km = KernelManager(connection_file=fullpath) + km.load_connection_file() + + kc = km.client() + kc.start_channels() + try: + send = kc.execute + except AttributeError: + send = kc.shell_channel.execute + if not hasattr(kc, 'iopub_channel'): + kc.iopub_channel = kc.sub_channel + + send('', silent=True) + try: + msg = kc.shell_channel.get_msg(timeout=1) + connected = True + socket = km.connect_iopub() + print('IPython monitor connected successfully') + break + except KeyboardInterrupt: + sys.exit(0) + except (Empty, KeyError): + continue + except Exception as e: + import traceback + traceback.print_exc() + finally: + if not connected: + kc.stop_channels() + + +def colorize(string, color, bold=False, bright=False): + if isinstance(color, str): + code = ''.join(('\033[', str(colors[color] + (90 if bright else 30)))) + else: + code = '\033[38;5;%d' % color + return ''.join((code, ';1' if bold else '', 'm', string, '\033[0m')) + + +def get_msgs(): + try: + kc.iopub_channel.flush() + return kc.iopub_channel.get_msgs() + except AttributeError: + msgs = [] + while True: + try: + msgs.append(kc.iopub_channel.get_msg(timeout=0.001)) + except Empty: + return msgs + + +if len(sys.argv) > 1: + term = open(sys.argv[1], 'w') + sys.stdout = term +else: + msg_id = send('import os as _os; _tty = _os.ttyname(1)', silent=True, + user_expressions=dict(_tty='_tty')) + while True: + try: + msg = kc.shell_channel.get_msg(timeout=1.0) + if msg['parent_header']['msg_id'] == msg_id: + sys.stdout = open(ast.literal_eval( + msg['content']['user_expressions'] + ['_tty']['data']['text/plain']), 'w+') + break + except Empty: + continue + + +class IPythonMonitor(object): + + def __init__(self): + self.clients = set() + self.execution_count_id = None + self.last_msg_type = None # Only set when text written to stdout + self.last_execution_count = 0 + + def print_prompt(self, start='In', color=28, num_color=46, count_offset=0): + count = str(self.last_execution_count + count_offset) + sys.stdout.write(colorize(start.rstrip() + ' [', color)) + sys.stdout.write(colorize(count, num_color, bold=True)) + sys.stdout.write(colorize(']: ', color)) + return '%s [%s]: ' % (start.strip(), count) + + def listen(self): + while socket.recv(): + for msg in get_msgs(): + msg_type = msg['msg_type'] + + if msg_type == 'shutdown_reply': + sys.exit(0) + + client = msg['parent_header'].get('session', '') + if (client and msg_type in ('execute_input', 'pyin') and + msg['content']['code'] == '"_vim_client";_=_;__=__'): + self.clients.add(client) + continue + if client not in self.clients: + continue + + getattr(self, msg_type, self.other)(msg) + sys.stdout.flush() + + def pyin(self, msg): + self.last_execution_count = msg['content']['execution_count'] + sys.stdout.write('\r') + dots = ' ' * (len(self.print_prompt().rstrip()) - 1) + ': ' + code = highlight(msg['content']['code'], lexer, formatter) + output = code.rstrip().replace('\n', '\n' + colorize(dots, 28)) + sys.stdout.write(output) + self.execution_count_id = msg['parent_header']['msg_id'] + self.last_msg_type = msg['msg_type'] + + def pyout(self, msg, prompt=True, spaces=''): + if 'execution_count' in msg['content']: + self.last_execution_count = msg['content']['execution_count'] + self.execution_count_id = msg['parent_header']['msg_id'] + output = msg['content']['data']['text/plain'] + if prompt: + self.print_prompt('\nOut', 196, 196) + sys.stdout.write(('\n' if '\n' in output else '') + output) + else: + sys.stdout.write(output) + self.last_msg_type = msg['msg_type'] + + def display_data(self, msg): + sys.stdout.write('\n') + self.pyout(msg, prompt=False) + + def pyerr(self, msg): + for line in msg['content']['traceback']: + sys.stdout.write('\n' + line) + if self.last_msg_type not in ('execute_input', 'pyin'): + self.print_prompt('\nIn') + self.last_msg_type = msg['msg_type'] + + def stream(self, msg): + if self.last_msg_type not in ('pyerr', 'error', 'stream'): + sys.stdout.write('\n') + try: + data = msg['content']['data'] + except KeyError: + data = msg['content']['text'] + sys.stdout.write(colorize(data, 'cyan', bright=True)) + self.last_msg_type = msg['msg_type'] + + def status(self, msg): + if (msg['content']['execution_state'] == 'idle' and + msg['parent_header']['msg_id'] == self.execution_count_id): + self.print_prompt('\nIn', count_offset=1) + self.execution_count_id = None + + def clear_output(self, msg): + if self.last_msg_type in ('execute_input', 'pyin'): + print('\n') + print('\033[2K\r', file=sys.stdout, end='') + + def other(self, msg): + print('msg_type = %s' % str(msg['msg_type'])) + print('msg = %s' % str(msg)) + + execute_input = pyin + execute_result = pyout + error = pyerr + + +monitor = IPythonMonitor() +monitor.listen()