1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-04-13 22:39:10 +08:00

Add deoplete into bundle dir

This commit is contained in:
Shidong Wang 2020-06-18 23:07:37 +08:00
parent a466c0d700
commit 003ba8e5ff
80 changed files with 11424 additions and 3 deletions

View File

@ -68,13 +68,13 @@ function! SpaceVim#layers#autocomplete#plugins() abort
call add(plugins, ['neoclide/coc.nvim', {'merged': 0, 'rev': 'release'}])
endif
elseif g:spacevim_autocomplete_method ==# 'deoplete'
call add(plugins, ['Shougo/deoplete.nvim', {
call add(plugins, [g:_spacevim_root_dir . 'bundle/deoplete.nvim', {
\ 'on_event' : 'InsertEnter',
\ 'loadconf' : 1,
\ }])
if !has('nvim')
call add(plugins, ['SpaceVim/nvim-yarp', {'merged': 0}])
call add(plugins, ['SpaceVim/vim-hug-neovim-rpc', {'merged': 0}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/nvim-yarp', {'merged': 0}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/vim-hug-neovim-rpc', {'merged': 0}])
endif
elseif g:spacevim_autocomplete_method ==# 'asyncomplete'
call add(plugins, ['prabirshrestha/asyncomplete.vim', {

View File

@ -0,0 +1,51 @@
**Warning: I will close the issue without the minimal init.vim and the reproduction instructions.**
# Problems summary
## Expected
## Environment Information
* deoplete version (SHA1):
* OS:
* neovim/Vim `:version` output:
* `:checkhealth` or `:CheckHealth` result(neovim only):
## Provide a minimal init.vim/vimrc with less than 50 lines (Required!)
```vim
" Your minimal init.vim/vimrc
set runtimepath+=~/path/to/deoplete.nvim/
let g:deoplete#enable_at_startup = 1
" For Vim only
"set runtimepath+=~/path/to/nvim-yarp/
"set runtimepath+=~/path/to/vim-hug-neovim-rpc/
```
## How to reproduce the problem from neovim/Vim startup (Required!)
1. foo
2. bar
3. baz
## Generate a logfile if appropriate
1. export NVIM_PYTHON_LOG_FILE=/tmp/log
2. export NVIM_PYTHON_LOG_LEVEL=DEBUG
3. nvim -u minimal.vimrc
4. some works
5. cat /tmp/log_{PID}
## Screenshot (if possible)
## Upload the log file

7
bundle/deoplete.nvim/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.py[cod]
doc/tags
vim-themis
.cache
.mypy_cache
.pytest_cache
tags

View File

@ -0,0 +1,22 @@
dist: xenial
language: python
python:
- 3.6
- 3.7
install:
- eval "$(curl -Ss https://raw.githubusercontent.com/neovim/bot-ci/master/scripts/travis-setup.sh) nightly-x64"
- make install
env:
global:
- PATH=$HOME/neovim/bin:$PATH
- PYTEST_ADDOPTS=--cov rplugin/python3/deoplete
script:
- make --keep-going test lint
- coverage report -m --skip-covered
- coverage xml
- bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -X fix -X search -X xcode -f coverage.xml

View File

@ -0,0 +1,21 @@
License: MIT license
AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
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.

View File

@ -0,0 +1,31 @@
PATH := ./vim-themis/bin:$(PATH)
export THEMIS_VIM := nvim
export THEMIS_ARGS := -e -s --headless
export THEMIS_HOME := ./vim-themis
install: vim-themis
pip install --upgrade -r test/requirements.txt
install-user: vim-themis
pip install --user --upgrade -r test/requirements.txt
lint:
vint --version
vint plugin
vint autoload
flake8 --version
flake8 rplugin
mypy --version
mypy --ignore-missing-imports --follow-imports=skip --strict rplugin/python3/deoplete
test:
themis --version
themis test/autoload/*
pytest --version
pytest
vim-themis:
git clone --depth 1 https://github.com/thinca/vim-themis $@
.PHONY: install lint test

View File

@ -0,0 +1,159 @@
# deoplete.nvim
> Dark powered asynchronous completion framework for neovim/Vim8
[![Build Status](https://travis-ci.org/Shougo/deoplete.nvim.svg?branch=master)](https://travis-ci.org/Shougo/deoplete.nvim)
[![Join the chat at https://gitter.im/Shougo/deoplete.nvim](https://badges.gitter.im/Shougo/deoplete.nvim.svg)](https://gitter.im/Shougo/deoplete.nvim?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Doc](https://img.shields.io/badge/doc-%3Ah%20deoplete-orange.svg)](doc/deoplete.txt)
Deoplete is the abbreviation of "dark powered neo-completion". It
provides an extensible and asynchronous completion framework for
neovim/Vim8.
deoplete will display completions via `complete()` by default.
Here are some [completion sources](https://github.com/Shougo/deoplete.nvim/wiki/Completion-Sources) specifically made for deoplete.nvim.
<!-- vim-markdown-toc GFM -->
- [Install](#install)
- [Requirements](#requirements)
- [Configuration](#configuration)
- [Screenshots](#screenshots)
<!-- vim-markdown-toc -->
## Install
**Note:** deoplete requires Neovim (0.3.0+ and of course, **latest** is
recommended) or Vim8 with Python 3.6.1+ and timers enabled. See
[requirements](#requirements) if you aren't sure whether you have this.
Note: deoplete requires msgpack package 1.0.0+.
Please install/upgrade msgpack package by pip.
https://github.com/msgpack/msgpack-python
Note: If you really need to use older msgpack, please use deoplete ver.5.2
instead.
https://github.com/Shougo/deoplete.nvim/releases/tag/5.2
For vim-plug
```viml
if has('nvim')
Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
else
Plug 'Shougo/deoplete.nvim'
Plug 'roxma/nvim-yarp'
Plug 'roxma/vim-hug-neovim-rpc'
endif
let g:deoplete#enable_at_startup = 1
```
For dein.vim
```viml
call dein#add('Shougo/deoplete.nvim')
if !has('nvim')
call dein#add('roxma/nvim-yarp')
call dein#add('roxma/vim-hug-neovim-rpc')
endif
let g:deoplete#enable_at_startup = 1
```
For manual installation(not recommended)
1. Extract the files and put them in your Neovim or .vim directory
(usually `$XDG_CONFIG_HOME/nvim/`).
2. Write `call deoplete#enable()` or `let g:deoplete#enable_at_startup = 1` in
your `init.vim`
### Requirements
deoplete requires Neovim or Vim8 with `if_python3`.
If `:echo has("python3")` returns `1`, then you have python 3 support; otherwise, see below.
You can enable Python3 interface with pip:
pip3 install --user pynvim
Please install nvim-yarp and vim-hug-neovim-rpc for Vim8.
- <https://github.com/roxma/nvim-yarp>
- <https://github.com/roxma/vim-hug-neovim-rpc>
**Note: Python3 must be enabled before updating remote plugins**
If Deoplete was installed prior to Python support being added to Neovim,
`:UpdateRemotePlugins` should be executed manually in order to enable
auto-completion.
**Note: deoplete needs pynvim ver.0.3.0+.**
You need update pynvim module.
pip3 install --user --upgrade pynvim
If you want to read the Neovim-python/python3 interface install documentation,
you should read `:help provider-python` and the Wiki.
<https://github.com/deoplete-plugins/deoplete-jedi/wiki/Setting-up-Python-for-Neovim>
## Configuration
```vim
" Use deoplete.
let g:deoplete#enable_at_startup = 1
```
See `:help deoplete-options` for a complete list of options.
## Screenshots
Deoplete for JavaScript
<https://www.youtube.com/watch?v=oanoPTpiSF4>
![File Name Completion](https://cloud.githubusercontent.com/assets/7141867/11717027/a99cac54-9f73-11e5-91ce-bce9274692e4.png)
![Omni Completion](https://cloud.githubusercontent.com/assets/7141867/11717030/ae809a28-9f73-11e5-8c12-79fe9c460401.png)
![Neosnippets and neco-ghc integration](https://cloud.githubusercontent.com/assets/7141867/11717032/b4159c0e-9f73-11e5-91ee-404e6390366a.png)
![deoplete + echodoc integration](https://github.com/archSeer/nvim-elixir/blob/master/autocomplete.gif)
![deoplete + deoplete-go integration](https://camo.githubusercontent.com/cfdefba43971bd44d466ead357bb296e38d7f88c/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f6c344b6930316d30314939424f485745302f67697068792e676966)
![deoplete + deoplete-typescript integration](https://github.com/mhartington/deoplete-typescript/blob/master/deoplete-tss.gif)
![Python completion using deoplete-jedi](https://cloud.githubusercontent.com/assets/3712731/17458493/8e10d1c0-5c44-11e6-8bd9-964f45365962.gif)
![C++ completion using clang_complete](https://cloud.githubusercontent.com/assets/3712731/17458501/cf88f89e-5c44-11e6-89a4-b4646aaa8021.gif)
![Java completion using vim-javacomplete2](https://cloud.githubusercontent.com/assets/3712731/17458504/f075e76a-5c44-11e6-97d5-c5525f61c4a9.gif)
![Vim Script completion using neco-vim](https://cloud.githubusercontent.com/assets/3712731/17461000/660e15be-5caf-11e6-8c02-eb9f9c169f3c.gif)
![C# completion using deoplete-omnisharp](https://camo.githubusercontent.com/f429dc72f91b25619980dbb9d436065ba3fb0a44/68747470733a2f2f692e696d6775722e636f6d2f464e634c4441752e676966)
![Register/Extract list completions](https://camo.githubusercontent.com/6a6df993ad0e05c014c72c8f8702447f9b34ad90/68747470733a2f2f692e696d6775722e636f6d2f5131663731744a2e676966)
![FSharp completion using deopletefs](https://github.com/callmekohei/deoplete-fsharp/blob/master/pic/sample.gif)
![Typescript](https://user-images.githubusercontent.com/29815830/36537450-bfbf4884-1802-11e8-8ad4-dd4a0dccfed3.png)
![Javascript](https://user-images.githubusercontent.com/29815830/36537514-ef01ef7a-1802-11e8-944e-c33017dfbe2b.png)
![Css, scss, sass](https://user-images.githubusercontent.com/29815830/36537545-1184f10a-1803-11e8-81a1-097222a58752.png)
![Html](https://user-images.githubusercontent.com/29815830/36537602-40b19848-1803-11e8-8ac8-49b3b9ba2094.png)
![My custom snippets](https://user-images.githubusercontent.com/29815830/36537646-6578262e-1803-11e8-9bff-64874a606150.png)
![C++ with cquery lang server](https://user-images.githubusercontent.com/1750795/38780762-7c74e51e-40a9-11e8-92f9-dee921555865.png)
![Rust using rls](https://user-images.githubusercontent.com/1750795/38780764-8524b0b8-40a9-11e8-91bc-6e4148c398a3.png)
![Ruby dictionary completion](https://user-images.githubusercontent.com/1314340/44786516-5bb57a00-abcf-11e8-8687-492fa5f9f905.gif)

View File

@ -0,0 +1,90 @@
"=============================================================================
" FILE: deoplete.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
function! deoplete#initialize() abort
return deoplete#init#_initialize()
endfunction
function! deoplete#is_enabled() abort
return deoplete#init#_is_handler_enabled()
endfunction
function! deoplete#enable() abort
if has('vim_starting')
augroup deoplete
autocmd!
autocmd VimEnter * call deoplete#enable()
augroup END
return 1
endif
if deoplete#initialize() && deoplete#is_enabled()
return 1
endif
return deoplete#init#_enable_handler()
endfunction
function! deoplete#disable() abort
call deoplete#initialize()
return deoplete#init#_disable_handler()
endfunction
function! deoplete#toggle() abort
call deoplete#initialize()
return deoplete#is_enabled() ?
\ deoplete#init#_disable_handler() :
\ deoplete#init#_enable_handler()
endfunction
function! deoplete#enable_logging(level, logfile) abort
let g:deoplete#_logging = {'level': a:level, 'logfile': a:logfile}
call deoplete#util#rpcnotify('deoplete_enable_logging', {})
endfunction
function! deoplete#send_event(event, ...) abort
if &l:previewwindow
return
endif
let sources = deoplete#util#convert2list(get(a:000, 0, []))
call deoplete#util#rpcnotify('deoplete_on_event',
\ {'event': a:event, 'sources': sources})
endfunction
function! deoplete#complete() abort
return deoplete#mapping#_dummy('deoplete#mapping#_complete')
endfunction
function! deoplete#auto_complete(...) abort
return deoplete#handler#_completion_begin(get(a:000, 0, 'Async'))
endfunction
function! deoplete#manual_complete(...) abort
if !deoplete#is_enabled()
return ''
endif
call deoplete#init#_prev_completion()
" Start complete.
return "\<C-r>=deoplete#mapping#_rpcrequest_wrapper("
\ . string(get(a:000, 0, [])) . ")\<CR>"
endfunction
function! deoplete#close_popup() abort
call deoplete#handler#_skip_next_completion()
return pumvisible() ? "\<C-y>" : ''
endfunction
function! deoplete#smart_close_popup() abort
call deoplete#handler#_skip_next_completion()
return pumvisible() ? "\<C-e>" : ''
endfunction
function! deoplete#cancel_popup() abort
call deoplete#handler#_skip_next_completion()
return pumvisible() ? "\<C-e>" : ''
endfunction
function! deoplete#insert_candidate(number) abort
return deoplete#mapping#_insert_candidate(a:number)
endfunction
function! deoplete#undo_completion() abort
return deoplete#mapping#_undo_completion()
endfunction
function! deoplete#complete_common_string() abort
return deoplete#mapping#_complete_common_string()
endfunction

View File

@ -0,0 +1,62 @@
# ============================================================================
# FILE: _main.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import sys
import io
from importlib.util import find_spec
if find_spec('pynvim'):
from pynvim import attach
else:
from neovim import attach
def attach_vim(serveraddr):
if len(serveraddr.split(':')) == 2:
serveraddr, port = serveraddr.split(':')
port = int(port)
vim = attach('tcp', address=serveraddr, port=port)
else:
vim = attach('socket', path=serveraddr)
# sync path
for path in vim.call(
'globpath', vim.options['runtimepath'],
'rplugin/python3', 1).split('\n'):
sys.path.append(path)
# Remove current path
del sys.path[0]
return vim
class RedirectStream(io.IOBase):
def __init__(self, handler):
self.handler = handler
def write(self, line):
self.handler(line)
def writelines(self, lines):
self.handler('\n'.join(lines))
def main(serveraddr):
vim = attach_vim(serveraddr)
from deoplete.child import Child
from deoplete.util import error_tb
stdout = sys.stdout
sys.stdout = RedirectStream(lambda data: vim.out_write(data))
sys.stderr = RedirectStream(lambda data: vim.err_write(data))
try:
child = Child(vim)
child.main_loop(stdout)
except Exception as exc:
error_tb(vim, 'Error in child: %r' % exc)
if __name__ == '__main__':
main(sys.argv[1])

View File

@ -0,0 +1,177 @@
"=============================================================================
" FILE: custom.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
function! deoplete#custom#_init() abort
let s:custom = {}
let s:custom.source = {}
let s:custom.source._ = {}
let s:custom.option = deoplete#init#_option()
let s:custom.filter = {}
let s:cached = {}
let s:cached.option = {}
let s:cached.filter = {}
let s:cached.buffer_option = {}
let s:cached.source_vars = {}
endfunction
function! deoplete#custom#_init_buffer() abort
let b:custom = {}
let b:custom.option = {}
let b:custom.source_vars = {}
let b:custom.filter = {}
endfunction
function! deoplete#custom#_update_cache() abort
if !exists('s:custom')
call deoplete#custom#_init()
endif
let custom_buffer = deoplete#custom#_get_buffer()
let s:cached.option = copy(s:custom.option)
let s:cached.buffer_option = copy(custom_buffer.option)
call extend(s:cached.option, s:cached.buffer_option)
let s:cached.source_vars = {}
for [name, source] in items(s:custom.source)
let s:cached.source_vars[name] = get(source, 'vars', {})
endfor
for [name, vars] in items(custom_buffer.source_vars)
if !has_key(s:cached.source_vars, name)
let s:cached.source_vars[name] = {}
endif
call extend(s:cached.source_vars[name], vars)
endfor
let s:cached.filter = {}
for [name, vars] in items(s:custom.filter)
let s:cached.filter[name] = vars
endfor
for [name, vars] in items(custom_buffer.filter)
if !has_key(s:cached.filter, name)
let s:cached.filter[name] = {}
endif
call extend(s:cached.filter[name], vars)
endfor
endfunction
function! deoplete#custom#_get() abort
if !exists('s:custom')
call deoplete#custom#_init()
endif
return s:custom
endfunction
function! deoplete#custom#_get_buffer() abort
if !exists('b:custom')
call deoplete#custom#_init_buffer()
endif
return b:custom
endfunction
function! deoplete#custom#_get_source(source_name) abort
let custom = deoplete#custom#_get().source
if !has_key(custom, a:source_name)
let custom[a:source_name] = {}
endif
return custom[a:source_name]
endfunction
function! deoplete#custom#_get_option(name) abort
return s:cached.option[a:name]
endfunction
function! deoplete#custom#_get_filetype_option(name, filetype, default) abort
let buffer_option = s:cached.buffer_option
if has_key(buffer_option, a:name)
" Use buffer_option instead
return buffer_option[a:name]
endif
let option = s:cached.option[a:name]
let filetype = has_key(option, a:filetype) ? a:filetype : '_'
return get(option, filetype, a:default)
endfunction
function! deoplete#custom#_get_source_vars(name) abort
return get(s:cached.source_vars, a:name, {})
endfunction
function! deoplete#custom#_get_filter(name) abort
return get(s:cached.filter, a:name, {})
endfunction
function! deoplete#custom#source(source_name, name_or_dict, ...) abort
for key in deoplete#util#split(a:source_name)
let custom_source = deoplete#custom#_get_source(key)
call s:set_custom(custom_source, a:name_or_dict, get(a:000, 0, ''))
endfor
endfunction
function! deoplete#custom#var(source_name, name_or_dict, ...) abort
for key in deoplete#util#split(a:source_name)
let custom_source = deoplete#custom#_get_source(key)
let vars = get(custom_source, 'vars', {})
call s:set_custom(vars, a:name_or_dict, get(a:000, 0, ''))
call deoplete#custom#source(key, 'vars', vars)
endfor
endfunction
function! deoplete#custom#buffer_var(source_name, name_or_dict, ...) abort
let custom = deoplete#custom#_get_buffer().source_vars
for key in deoplete#util#split(a:source_name)
if !has_key(custom, key)
let custom[key] = {}
endif
let vars = custom[key]
call s:set_custom(vars, a:name_or_dict, get(a:000, 0, ''))
endfor
endfunction
function! deoplete#custom#filter(filter_name, name_or_dict, ...) abort
let custom = deoplete#custom#_get().filter
for key in deoplete#util#split(a:filter_name)
if !has_key(custom, key)
let custom[key] = {}
endif
let vars = custom[key]
call s:set_custom(vars, a:name_or_dict, get(a:000, 0, ''))
endfor
endfunction
function! deoplete#custom#buffer_filter(filter_name, name_or_dict, ...) abort
let custom = deoplete#custom#_get_buffer().filter
for key in deoplete#util#split(a:filter_name)
if !has_key(custom, key)
let custom[key] = {}
endif
let vars = custom[key]
call s:set_custom(vars, a:name_or_dict, get(a:000, 0, ''))
endfor
endfunction
function! deoplete#custom#option(name_or_dict, ...) abort
let custom = deoplete#custom#_get().option
call s:set_custom(custom, a:name_or_dict, get(a:000, 0, ''))
endfunction
function! deoplete#custom#buffer_option(name_or_dict, ...) abort
let custom = deoplete#custom#_get_buffer().option
call s:set_custom(custom, a:name_or_dict, get(a:000, 0, ''))
endfunction
function! s:set_custom(dest, name_or_dict, value) abort
if type(a:name_or_dict) == v:t_dict
call extend(a:dest, a:name_or_dict)
else
call s:set_value(a:dest, a:name_or_dict, a:value)
endif
endfunction
function! s:set_value(dest, name, value) abort
if type(a:value) == v:t_dict && !empty(a:value)
if !has_key(a:dest, a:name)
let a:dest[a:name] = {}
endif
call extend(a:dest[a:name], a:value)
else
let a:dest[a:name] = a:value
endif
endfunction

View File

@ -0,0 +1,400 @@
"=============================================================================
" FILE: handler.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
function! deoplete#handler#_init() abort
augroup deoplete
autocmd!
autocmd InsertLeave * call s:on_insert_leave()
autocmd CompleteDone * call s:on_complete_done()
augroup END
for event in [
\ 'InsertEnter', 'InsertLeave',
\ 'BufReadPost', 'BufWritePost',
\ 'VimLeavePre',
\ ]
call s:define_on_event(event)
endfor
if deoplete#custom#_get_option('on_text_changed_i')
call s:define_completion_via_timer('TextChangedI')
endif
if deoplete#custom#_get_option('on_insert_enter')
call s:define_completion_via_timer('InsertEnter')
endif
if deoplete#custom#_get_option('refresh_always')
if exists('##TextChangedP')
call s:define_completion_via_timer('TextChangedP')
else
call s:define_completion_via_timer('InsertCharPre')
endif
endif
" Note: Vim 8 GUI(MacVim and Win32) is broken
" dummy timer call is needed before complete()
if !has('nvim') && has('gui_running')
\ && (has('gui_macvim') || has('win32'))
let s:dummy_timer = timer_start(200, {timer -> 0}, {'repeat': -1})
endif
if deoplete#util#has_yarp()
" To fix "RuntimeError: Event loop is closed" issue
" Note: Workaround
autocmd deoplete VimLeavePre * call s:kill_yarp()
endif
endfunction
function! deoplete#handler#_do_complete() abort
let context = g:deoplete#_context
let event = get(context, 'event', '')
if s:is_exiting() || v:insertmode !=# 'i' || s:check_input_method()
return
endif
if !has_key(context, 'candidates')
\ || deoplete#util#get_input(context.event) !=# context.input
return
endif
let prev = g:deoplete#_prev_completion
let prev.event = context.event
let prev.input = context.input
let prev.candidates = context.candidates
let prev.complete_position = context.complete_position
let prev.linenr = line('.')
let auto_popup = deoplete#custom#_get_option(
\ 'auto_complete_popup') !=# 'manual'
" Enable auto refresh when popup is displayed
if deoplete#util#check_popup()
let auto_popup = v:true
endif
if context.event ==# 'Manual'
let context.event = ''
elseif !exists('g:deoplete#_saved_completeopt') && auto_popup
call deoplete#mapping#_set_completeopt()
endif
if auto_popup
call feedkeys("\<Plug>_", 'i')
endif
endfunction
function! deoplete#handler#_check_omnifunc(context) abort
let prev = g:deoplete#_prev_completion
let blacklist = ['LanguageClient#complete']
if a:context.event ==# 'Manual'
\ || &l:omnifunc ==# ''
\ || index(blacklist, &l:omnifunc) >= 0
\ || prev.input ==# a:context.input
\ || s:check_input_method()
\ || deoplete#custom#_get_option('auto_complete_popup') ==# 'manual'
return
endif
for filetype in a:context.filetypes
for pattern in deoplete#util#convert2list(
\ deoplete#custom#_get_filetype_option(
\ 'omni_patterns', filetype, ''))
if pattern !=# '' && a:context.input =~# '\%('.pattern.'\)$'
let g:deoplete#_context.candidates = []
let prev.event = a:context.event
let prev.input = a:context.input
let prev.candidates = []
if &completeopt =~# 'noselect'
call deoplete#mapping#_set_completeopt()
call feedkeys("\<C-x>\<C-o>", 'in')
else
call deoplete#util#print_error(
\ 'omni_patterns feature is disabled.')
call deoplete#util#print_error(
\ 'You need to set "noselect" in completeopt option.')
endif
return 1
endif
endfor
endfor
endfunction
function! s:completion_timer_start(event) abort
if exists('s:completion_timer')
call s:completion_timer_stop()
endif
let delay = deoplete#custom#_get_option('auto_complete_delay')
if delay > 0
let s:completion_timer = timer_start(
\ delay, {-> deoplete#handler#_completion_begin(a:event)})
else
call deoplete#handler#_completion_begin(a:event)
endif
endfunction
function! s:completion_timer_stop() abort
if !exists('s:completion_timer')
return
endif
call timer_stop(s:completion_timer)
unlet s:completion_timer
endfunction
function! s:check_prev_completion(event) abort
let prev = g:deoplete#_prev_completion
if a:event ==# 'Async' || a:event ==# 'Update' || mode() !=# 'i'
\ || empty(get(prev, 'candidates', []))
\ || s:check_input_method()
return
endif
let input = deoplete#util#get_input(a:event)
let complete_str = matchstr(input, '\w\+$')
let min_pattern_length = deoplete#custom#_get_option('min_pattern_length')
if prev.linenr != line('.') || len(complete_str) < min_pattern_length
return
endif
call deoplete#mapping#_set_completeopt()
let mode = deoplete#custom#_get_option('prev_completion_mode')
let candidates = copy(prev.candidates)
if mode ==# 'filter' || mode ==# 'length'
let input = input[prev.complete_position :]
let escaped_input = escape(input, '~\.^$[]*')
let pattern = substitute(escaped_input, '\w', '\\w*\0', 'g')
call filter(candidates, 'v:val.word =~? pattern')
if mode ==# 'length'
call filter(candidates, 'len(v:val.word) > len(input)')
endif
elseif mode ==# 'mirror'
" pass
else
return
endif
let g:deoplete#_filtered_prev = {
\ 'complete_position': prev.complete_position,
\ 'candidates': candidates,
\ }
call feedkeys("\<Plug>+", 'i')
endfunction
function! deoplete#handler#_async_timer_start() abort
let delay = deoplete#custom#_get_option('auto_refresh_delay')
if delay <= 0
return
endif
call timer_start(max([20, delay]), {-> deoplete#auto_complete()})
endfunction
function! deoplete#handler#_completion_begin(event) abort
call deoplete#custom#_update_cache()
if s:is_skip(a:event)
let g:deoplete#_context.candidates = []
return
endif
call s:check_prev_completion(a:event)
if a:event !=# 'Update' && a:event !=# 'Async'
call deoplete#init#_prev_completion()
endif
call deoplete#util#rpcnotify(
\ 'deoplete_auto_completion_begin', {'event': a:event})
endfunction
function! s:is_skip(event) abort
if a:event ==# 'TextChangedP' && !empty(v:completed_item)
return 1
endif
" Note: The check is needed for <C-y> mapping
if s:is_skip_prev_text(a:event)
return 1
endif
if s:is_skip_text(a:event)
" Close the popup
if deoplete#util#check_popup()
call feedkeys("\<Plug>_", 'i')
endif
return 1
endif
let auto_complete = deoplete#custom#_get_option('auto_complete')
if &paste
\ || (a:event !=# 'Manual' && a:event !=# 'Update' && !auto_complete)
\ || (&l:completefunc !=# '' && &l:buftype =~# 'nofile')
\ || v:insertmode !=# 'i'
return 1
endif
return 0
endfunction
function! s:is_skip_prev_text(event) abort
let input = deoplete#util#get_input(a:event)
" Note: Use g:deoplete#_context is needed instead of
" g:deoplete#_prev_completion
let prev_input = get(g:deoplete#_context, 'input', '')
if input ==# prev_input
\ && input !=# ''
\ && a:event !=# 'Manual'
\ && a:event !=# 'Async'
\ && a:event !=# 'Update'
\ && a:event !=# 'TextChangedP'
return 1
endif
" Note: It fixes insert first candidate automatically problem
if a:event ==# 'Update' && prev_input !=# '' && input !=# prev_input
return 1
endif
return 0
endfunction
function! s:is_skip_text(event) abort
let input = deoplete#util#get_input(a:event)
let lastchar = matchstr(input, '.$')
let skip_multibyte = deoplete#custom#_get_option('skip_multibyte')
if skip_multibyte && len(lastchar) != strwidth(lastchar)
\ && empty(get(b:, 'eskk', []))
return 1
endif
let displaywidth = strdisplaywidth(input) + 1
let is_virtual = virtcol('.') >= displaywidth
if &l:formatoptions =~# '[tca]' && &l:textwidth > 0
\ && displaywidth >= &l:textwidth
if &l:formatoptions =~# '[ta]'
\ || !empty(filter(deoplete#util#get_syn_names(),
\ "v:val ==# 'Comment'"))
\ || is_virtual
return 1
endif
endif
let skip_chars = deoplete#custom#_get_option('skip_chars')
return (a:event !=# 'Manual' && input !=# ''
\ && index(skip_chars, input[-1:]) >= 0)
endfunction
function! s:check_input_method() abort
return exists('*getimstatus') && getimstatus()
endfunction
function! s:define_on_event(event) abort
if !exists('##' . a:event)
return
endif
execute 'autocmd deoplete' a:event
\ '* call deoplete#send_event('.string(a:event).')'
endfunction
function! s:define_completion_via_timer(event) abort
if !exists('##' . a:event)
return
endif
execute 'autocmd deoplete' a:event
\ '* call s:completion_timer_start('.string(a:event).')'
endfunction
function! s:on_insert_leave() abort
call deoplete#mapping#_restore_completeopt()
let g:deoplete#_context = {}
call deoplete#init#_prev_completion()
if &cpoptions =~# '$'
" If 'cpoptions' includes '$' with popup, redraw problem exists.
redraw
endif
endfunction
function! s:on_complete_done() abort
if get(v:completed_item, 'word', '') ==# ''
return
endif
call deoplete#handler#_skip_next_completion()
let user_data = get(v:completed_item, 'user_data', '')
if type(user_data) !=# v:t_string || user_data ==# ''
return
endif
try
call s:substitute_suffix(json_decode(user_data))
catch /.*/
endtry
endfunction
function! s:substitute_suffix(user_data) abort
if !deoplete#custom#_get_option('complete_suffix')
\ || !has_key(a:user_data, 'old_suffix')
\ || !has_key(a:user_data, 'new_suffix')
return
endif
let old_suffix = a:user_data.old_suffix
let new_suffix = a:user_data.new_suffix
let next_text = deoplete#util#get_next_input('CompleteDone')
if stridx(next_text, old_suffix) != 0
return
endif
let next_text = new_suffix . next_text[len(old_suffix):]
call setline('.', deoplete#util#get_input('CompleteDone') . next_text)
endfunction
function! deoplete#handler#_skip_next_completion() abort
if !exists('g:deoplete#_context')
return
endif
let input = deoplete#util#get_input('CompleteDone')
if input !~# '[/.]$'
let g:deoplete#_context.input = input
endif
call deoplete#mapping#_restore_completeopt()
call deoplete#init#_prev_completion()
endfunction
function! s:is_exiting() abort
return exists('v:exiting') && v:exiting != v:null
endfunction
function! s:kill_yarp() abort
if !exists('g:deoplete#_yarp')
return
endif
if g:deoplete#_yarp.job_is_dead
return
endif
let job = g:deoplete#_yarp.job
if !has('nvim') && !exists('g:yarp_jobstart')
" Get job object from vim-hug-neovim-rpc
let job = g:_neovim_rpc_jobs[job].job
endif
if has('nvim')
call jobstop(job)
else
call job_stop(job, 'kill')
endif
let g:deoplete#_yarp.job_is_dead = 1
endfunction

View File

@ -0,0 +1,298 @@
"=============================================================================
" FILE: init.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
if !exists('s:is_handler_enabled')
let s:is_handler_enabled = 0
endif
function! deoplete#init#_is_handler_enabled() abort
return s:is_handler_enabled
endfunction
function! deoplete#init#_initialize() abort
if exists('g:deoplete#_initialized')
return 1
endif
let g:deoplete#_initialized = v:false
call deoplete#init#_custom_variables()
call deoplete#custom#_update_cache()
call s:init_internal_variables()
" For context_filetype check
silent! call context_filetype#get()
if deoplete#init#_channel()
return 1
endif
call deoplete#mapping#_init()
endfunction
function! deoplete#init#_channel() abort
if !exists('g:deoplete#_serveraddr')
return 1
endif
let python3 = get(g:, 'python3_host_prog', 'python3')
if !executable(python3)
call deoplete#util#print_error(
\ string(python3) . ' is not executable.')
call deoplete#util#print_error(
\ 'You need to set g:python3_host_prog.')
endif
if has('nvim') && !has('nvim-0.3.0')
call deoplete#util#print_error('deoplete requires nvim 0.3.0+.')
return 1
endif
if !has('nvim') && v:version < 800
call deoplete#util#print_error('deoplete requires Vim 8.0+.')
return 1
endif
try
if deoplete#util#has_yarp()
let g:deoplete#_yarp = yarp#py3('deoplete')
call g:deoplete#_yarp.notify('deoplete_init')
else
" rplugin.vim may not be loaded on VimEnter
if !exists('g:loaded_remote_plugins')
runtime! plugin/rplugin.vim
endif
call _deoplete_init()
endif
catch
call deoplete#util#print_error(v:exception)
call deoplete#util#print_error(v:throwpoint)
if !has('python3')
call deoplete#util#print_error(
\ 'deoplete requires Python3 support("+python3").')
endif
if deoplete#init#_python_version_check()
call deoplete#util#print_error('deoplete requires Python 3.6.1+.')
endif
if deoplete#util#has_yarp()
if !exists('*yarp#py3')
call deoplete#util#print_error(
\ 'deoplete requires nvim-yarp plugin.')
endif
else
call deoplete#util#print_error(
\ 'deoplete failed to load. '
\ .'Try the :UpdateRemotePlugins command and restart Neovim. '
\ .'See also :checkhealth.')
endif
return 1
endtry
endfunction
function! deoplete#init#_channel_initialized() abort
return get(g:, 'deoplete#_initialized', v:false)
endfunction
function! deoplete#init#_enable_handler() abort
call deoplete#handler#_init()
let s:is_handler_enabled = 1
endfunction
function! deoplete#init#_disable_handler() abort
augroup deoplete
autocmd!
augroup END
let s:is_handler_enabled = 0
endfunction
function! s:init_internal_variables() abort
call deoplete#init#_prev_completion()
let g:deoplete#_context = {}
if !exists('g:deoplete#_logging')
let g:deoplete#_logging = {}
endif
unlet! g:deoplete#_initialized
try
let g:deoplete#_serveraddr =
\ deoplete#util#has_yarp() ?
\ neovim_rpc#serveraddr() : v:servername
if g:deoplete#_serveraddr ==# ''
" Use NVIM_LISTEN_ADDRESS
let g:deoplete#_serveraddr = $NVIM_LISTEN_ADDRESS
endif
catch
call deoplete#util#print_error(v:exception)
call deoplete#util#print_error(v:throwpoint)
if !has('python3')
call deoplete#util#print_error(
\ 'deoplete requires Python3 support("+python3").')
endif
if deoplete#util#has_yarp()
" Dummy call is needed to check exists()
call neovim_rpc#serveraddr()
if !exists('*neovim_rpc#serveraddr')
call deoplete#util#print_error(
\ 'deoplete requires vim-hug-neovim-rpc plugin in Vim.')
endif
endif
endtry
endfunction
function! deoplete#init#_custom_variables() abort
if get(g:, 'deoplete#disable_auto_complete', v:false)
call deoplete#custom#option('auto_complete', v:false)
endif
call s:check_custom_option(
\ 'g:deoplete#auto_complete_delay',
\ 'auto_complete_delay')
call s:check_custom_option(
\ 'g:deoplete#auto_refresh_delay',
\ 'auto_refresh_delay')
call s:check_custom_option(
\ 'g:deoplete#camel_case',
\ 'camel_case')
call s:check_custom_option(
\ 'g:deoplete#ignore_case',
\ 'ignore_case')
call s:check_custom_option(
\ 'g:deoplete#ignore_sources',
\ 'ignore_sources')
call s:check_custom_option(
\ 'g:deoplete#keyword_patterns',
\ 'keyword_patterns')
call s:check_custom_option(
\ 'g:deoplete#max_list',
\ 'max_list')
call s:check_custom_option(
\ 'g:deoplete#num_processes',
\ 'num_processes')
call s:check_custom_option(
\ 'g:deoplete#auto_complete_start_length',
\ 'min_pattern_length')
call s:check_custom_option(
\ 'g:deoplete#enable_on_insert_enter',
\ 'on_insert_enter')
call s:check_custom_option(
\ 'g:deoplete#enable_profile',
\ 'profile')
call s:check_custom_option(
\ 'g:deoplete#enable_refresh_always',
\ 'refresh_always')
call s:check_custom_option(
\ 'g:deoplete#skip_chars',
\ 'skip_chars')
call s:check_custom_option(
\ 'g:deoplete#sources',
\ 'sources')
call s:check_custom_option(
\ 'g:deoplete#enable_smart_case',
\ 'smart_case')
call s:check_custom_option(
\ 'g:deoplete#enable_complete_suffix',
\ 'complete_suffix')
call s:check_custom_option(
\ 'g:deoplete#enable_yarp',
\ 'yarp')
" Source variables
call s:check_custom_var('file',
\ 'g:deoplete#file#enable_buffer_path',
\ 'enable_buffer_path')
call s:check_custom_var('omni',
\ 'g:deoplete#omni#input_patterns',
\ 'input_patterns')
call s:check_custom_var('omni',
\ 'g:deoplete#omni#functions',
\ 'functions')
endfunction
function! s:check_custom_var(source_name, old_var, new_var) abort
if !exists(a:old_var)
return
endif
call deoplete#util#print_error(
\ printf('%s is deprecated variable. '.
\ 'Please use deoplete#custom#var() instead.', a:old_var))
call deoplete#custom#var(a:source_name, a:new_var, eval(a:old_var))
endfunction
function! s:check_custom_option(old_var, new_var) abort
if !exists(a:old_var)
return
endif
call deoplete#util#print_error(
\ printf('%s is deprecated variable. '.
\ 'Please use deoplete#custom#option() instead.', a:old_var))
call deoplete#custom#option(a:new_var, eval(a:old_var))
endfunction
function! deoplete#init#_option() abort
" Note: HTML omni func use search().
return {
\ 'auto_complete': v:true,
\ 'auto_complete_delay': 0,
\ 'auto_complete_popup': 'auto',
\ 'auto_refresh_delay': 100,
\ 'camel_case': v:false,
\ 'candidate_marks': [],
\ 'check_stderr': v:true,
\ 'complete_suffix': v:true,
\ 'ignore_case': &ignorecase,
\ 'ignore_sources': {},
\ 'keyword_patterns': {'_': '[a-zA-Z_]\k*'},
\ 'max_list': 500,
\ 'min_pattern_length': 2,
\ 'num_processes': 4,
\ 'omni_patterns': {},
\ 'on_insert_enter': v:true,
\ 'on_text_changed_i': v:true,
\ 'prev_completion_mode': '',
\ 'profile': v:false,
\ 'refresh_always': v:true,
\ 'skip_chars': ['(', ')'],
\ 'skip_multibyte': v:false,
\ 'smart_case': &smartcase,
\ 'sources': {},
\ 'trigger_key': v:char,
\ 'yarp': v:false,
\ }
endfunction
function! deoplete#init#_prev_completion() abort
let g:deoplete#_prev_completion = {
\ 'event': '',
\ 'input': '',
\ 'linenr': -1,
\ 'candidates': [],
\ 'complete_position': -1,
\ }
endfunction
function! deoplete#init#_python_version_check() abort
python3 << EOF
import vim
import sys
vim.vars['deoplete#_python_version_check'] = (
sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro) < (3, 6, 1)
EOF
return get(g:, 'deoplete#_python_version_check', 0)
endfunction
function! deoplete#init#_msgpack_version_check() abort
python3 << EOF
import vim
import msgpack
vim.vars['deoplete#_msgpack_version'] = msgpack.version
vim.vars['deoplete#_msgpack_version_check'] = msgpack.version < (1, 0, 0)
EOF
return get(g:, 'deoplete#_msgpack_version_check', 0)
endfunction

View File

@ -0,0 +1,154 @@
"=============================================================================
" FILE: mapping.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
function! deoplete#mapping#_init() abort
" Note: The dummy function is needed for cpoptions bug in neovim
inoremap <expr><silent> <Plug>_
\ deoplete#mapping#_dummy('deoplete#mapping#_complete')
inoremap <expr><silent> <Plug>+
\ deoplete#mapping#_dummy('deoplete#mapping#_prev_complete')
endfunction
function! deoplete#mapping#_dummy(func) abort
return "\<C-r>=".a:func."()\<CR>"
endfunction
function! s:check_completion_info(candidates) abort
if !exists('*complete_info')
return 0
endif
let info = complete_info()
let noinsert = &completeopt =~# 'noinsert'
if (info.mode !=# '' && info.mode !=# 'eval')
\ || (noinsert && info.selected > 0)
\ || (!noinsert && info.selected >= 0)
\ || !has_key(g:deoplete#_context, 'complete_position')
return 1
endif
let input = getline('.')[: g:deoplete#_context.complete_position - 1]
if deoplete#util#check_eskk_phase_henkan()
\ && matchstr(input, '.$') =~# '[\u3040-\u304A]$'
return 0
endif
return 0
let old_candidates = sort(map(copy(info.items), 'v:val.word'))
return sort(map(copy(a:candidates), 'v:val.word')) ==# old_candidates
endfunction
function! deoplete#mapping#_complete() abort
if !has_key(g:deoplete#_context, 'candidates')
\ || s:check_completion_info(g:deoplete#_context.candidates)
\ || !&modifiable
return ''
endif
" echomsg string(g:deoplete#_context)
if empty(g:deoplete#_context.candidates) && deoplete#util#check_popup()
" Note: call complete() to close the popup
call complete(1, [])
return ''
endif
call complete(g:deoplete#_context.complete_position + 1,
\ g:deoplete#_context.candidates)
return ''
endfunction
function! deoplete#mapping#_prev_complete() abort
if s:check_completion_info(g:deoplete#_filtered_prev.candidates)
return ''
endif
call complete(g:deoplete#_filtered_prev.complete_position + 1,
\ g:deoplete#_filtered_prev.candidates)
return ''
endfunction
function! deoplete#mapping#_set_completeopt() abort
if exists('g:deoplete#_saved_completeopt')
return
endif
let g:deoplete#_saved_completeopt = &completeopt
set completeopt-=longest
set completeopt+=menuone
set completeopt-=menu
if &completeopt !~# 'noinsert\|noselect'
set completeopt+=noselect
endif
endfunction
function! deoplete#mapping#_restore_completeopt() abort
if exists('g:deoplete#_saved_completeopt')
let &completeopt = g:deoplete#_saved_completeopt
unlet g:deoplete#_saved_completeopt
endif
endfunction
function! deoplete#mapping#_rpcrequest_wrapper(sources) abort
return deoplete#util#rpcnotify(
\ 'deoplete_manual_completion_begin',
\ {
\ 'event': 'Manual',
\ 'sources': deoplete#util#convert2list(a:sources)
\ })
endfunction
function! deoplete#mapping#_undo_completion() abort
if empty(v:completed_item)
return ''
endif
let input = deoplete#util#get_input('')
if strridx(input, v:completed_item.word) !=
\ len(input) - len(v:completed_item.word)
return ''
endif
return repeat("\<C-h>", strchars(v:completed_item.word))
endfunction
function! deoplete#mapping#_complete_common_string() abort
if !deoplete#is_enabled()
return ''
endif
" Get cursor word.
let prev = g:deoplete#_prev_completion
if empty(prev)
return ''
endif
let complete_str = prev.input[prev.complete_position :]
let candidates = filter(copy(prev.candidates),
\ 'stridx(tolower(v:val.word), tolower(complete_str)) == 0')
if empty(candidates) || complete_str ==# ''
return ''
endif
let common_str = candidates[0].word
for candidate in candidates[1:]
while stridx(tolower(candidate.word), tolower(common_str)) != 0
let common_str = common_str[: -2]
endwhile
endfor
if common_str ==# '' || complete_str ==? common_str
return ''
endif
return (pumvisible() ? "\<C-e>" : '')
\ . repeat("\<BS>", strchars(complete_str)) . common_str
endfunction
function! deoplete#mapping#_insert_candidate(number) abort
let prev = g:deoplete#_prev_completion
let candidates = get(prev, 'candidates', [])
let word = get(candidates, a:number, {'word': ''}).word
if word ==# ''
return ''
endif
" Get cursor word.
let complete_str = prev.input[prev.complete_position :]
return (pumvisible() ? "\<C-e>" : '')
\ . repeat("\<BS>", strchars(complete_str)) . word
endfunction

View File

@ -0,0 +1,215 @@
"=============================================================================
" FILE: util.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
function! deoplete#util#print_error(string, ...) abort
let name = a:0 ? a:1 : 'deoplete'
echohl Error | echomsg printf('[%s] %s', name,
\ deoplete#util#string(a:string)) | echohl None
endfunction
function! deoplete#util#print_warning(string) abort
echohl WarningMsg | echomsg '[deoplete] '
\ . deoplete#util#string(a:string) | echohl None
endfunction
function! deoplete#util#print_debug(string) abort
echomsg '[deoplete] ' . deoplete#util#string(a:string)
endfunction
function! deoplete#util#convert2list(expr) abort
return type(a:expr) ==# v:t_list ? a:expr : [a:expr]
endfunction
function! deoplete#util#string(expr) abort
return type(a:expr) ==# v:t_string ? a:expr : string(a:expr)
endfunction
function! deoplete#util#get_input(event) abort
let mode = mode()
if a:event ==# 'InsertEnter'
let mode = 'i'
endif
let input = (mode ==# 'i' ? (col('.')-1) : col('.')) >= len(getline('.')) ?
\ getline('.') :
\ matchstr(getline('.'),
\ '^.*\%' . (mode ==# 'i' ? col('.') : col('.') - 1)
\ . 'c' . (mode ==# 'i' ? '' : '.'))
if a:event ==# 'InsertCharPre'
let input .= v:char
endif
return input
endfunction
function! deoplete#util#get_next_input(event) abort
return getline('.')[len(deoplete#util#get_input(a:event)) :]
endfunction
function! deoplete#util#vimoption2python(option) abort
return '[\w' . s:vimoption2python(a:option) . ']'
endfunction
function! deoplete#util#vimoption2python_not(option) abort
return '[^\w' . s:vimoption2python(a:option) . ']'
endfunction
function! s:vimoption2python(option) abort
let has_dash = 0
let patterns = []
for pattern in split(a:option, ',')
if pattern =~# '\d\+'
let pattern = substitute(pattern, '\d\+',
\ '\=nr2char(submatch(0))', 'g')
endif
if pattern ==# ''
" ,
call add(patterns, ',')
elseif pattern ==# '\'
call add(patterns, '\\')
elseif pattern ==# '-'
let has_dash = 1
else
" Avoid ambiguous Python 3 RE syntax for nested sets
if pattern =~# '^--'
let pattern = '\' . pattern
elseif pattern =~# '--$'
let pattern = split(pattern, '-')[0] . '-\-'
endif
call add(patterns, pattern)
endif
endfor
" Dash must be last.
if has_dash
call add(patterns, '-')
endif
return join(deoplete#util#uniq(patterns), '')
endfunction
function! deoplete#util#uniq(list) abort
let list = map(copy(a:list), '[v:val, v:val]')
let i = 0
let seen = {}
while i < len(list)
let key = string(list[i][1])
if has_key(seen, key)
call remove(list, i)
else
let seen[key] = 1
let i += 1
endif
endwhile
return map(list, 'v:val[0]')
endfunction
function! deoplete#util#get_syn_names() abort
if col('$') >= 200
return []
endif
let names = []
try
" Note: synstack() seems broken in concealed text.
for id in synstack(line('.'), (mode() ==# 'i' ? col('.')-1 : col('.')))
let name = synIDattr(id, 'name')
call add(names, name)
if synIDattr(synIDtrans(id), 'name') !=# name
call add(names, synIDattr(synIDtrans(id), 'name'))
endif
endfor
catch
" Ignore error
endtry
return names
endfunction
function! deoplete#util#neovim_version() abort
redir => v
silent version
redir END
return split(v, '\n')[0]
endfunction
function! deoplete#util#has_yarp() abort
return !has('nvim') || deoplete#custom#_get_option('yarp')
endfunction
function! deoplete#util#get_keyword_pattern(filetype) abort
let keyword_patterns = deoplete#custom#_get_option('keyword_patterns')
if empty(keyword_patterns)
let patterns = deoplete#custom#_get_filetype_option(
\ 'keyword_patterns', a:filetype, '')
else
let filetype = has_key(keyword_patterns, a:filetype) ? a:filetype : '_'
let patterns = get(keyword_patterns, filetype, '')
endif
let pattern = join(deoplete#util#convert2list(patterns), '|')
" Convert keyword.
let k_pattern = deoplete#util#vimoption2python(
\ &l:iskeyword . (&l:lisp ? ',-' : ''))
return substitute(pattern, '\\k', '\=k_pattern', 'g')
endfunction
function! deoplete#util#rpcnotify(method, context) abort
if !deoplete#init#_channel_initialized()
return ''
endif
let a:context['rpc'] = a:method
if deoplete#util#has_yarp()
if g:deoplete#_yarp.job_is_dead
return ''
endif
call g:deoplete#_yarp.notify(a:method, a:context)
else
call rpcnotify(g:deoplete#_channel_id, a:method, a:context)
endif
return ''
endfunction
" Compare versions. Return values is the distance between versions. Each
" version integer (from right to left) is an ascending power of 100.
"
" Example:
" '0.1.10' is (1 * 100) + 10, or 110.
" '1.2.3' is (1 * 10000) + (2 * 100) + 3, or 10203.
"
" Returns:
" <0 if a < b
" >0 if a > b
" 0 if versions are equal.
function! deoplete#util#versioncmp(a, b) abort
let a = map(split(a:a, '\.'), 'str2nr(v:val)')
let b = map(split(a:b, '\.'), 'str2nr(v:val)')
let l = min([len(a), len(b)])
let d = 0
" Only compare the parts that are common to both versions.
for i in range(l)
let d += (a[i] - b[i]) * pow(100, l - i - 1)
endfor
return d
endfunction
function! deoplete#util#split(string) abort
return split(a:string, '\s*,\s*')
endfunction
function! deoplete#util#check_eskk_phase_henkan() abort
if !exists('b:eskk') || empty(b:eskk)
return 0
endif
let preedit = eskk#get_preedit()
let phase = preedit.get_henkan_phase()
return phase is g:eskk#preedit#PHASE_HENKAN
endfunction
function! deoplete#util#check_popup() abort
return exists('*complete_info') && complete_info().mode ==# 'eval'
endfunction

View File

@ -0,0 +1,79 @@
"=============================================================================
" FILE: deoplete.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" TJ DeVries <devries.timothyj at gmail.com>
" License: MIT license
"=============================================================================
function! s:check_t_list() abort
if exists('v:t_list')
call health#report_ok('exists("v:t_list") was successful')
else
call health#report_error('exists("v:t_list") was not successful',
\ 'Deoplete requires neovim 0.3.0+!')
endif
endfunction
function! s:check_timers() abort
if has('timers')
call health#report_ok('has("timers") was successful')
else
call health#report_error('has("timers") was not successful',
\ 'Deoplete requires timers support("+timers").')
endif
endfunction
function! s:check_required_python() abort
if has('python3')
call health#report_ok('has("python3") was successful')
else
call health#report_error('has("python3") was not successful', [
\ 'Please install the python3 package for neovim.',
\ 'A good guide can be found here: ' .
\ 'https://github.com/tweekmonster/nvim-python-doctor/'
\ . 'wiki/Advanced:-Using-pyenv'
\ ]
\ )
endif
if !deoplete#init#_python_version_check()
call health#report_ok('Require Python 3.6.1+ was successful')
else
call health#report_error(
\ 'Require Python 3.6.1+ was not successful',
\ 'Please use Python 3.6.1+.')
endif
endfunction
function! s:check_required_msgpack() abort
if !deoplete#init#_msgpack_version_check()
call health#report_ok('Require msgpack 1.0.0+ was successful')
else
call health#report_error(
\ 'Require msgpack 1.0.0+ was not successful',
\ 'Please install/upgrade msgpack 1.0.0+.')
endif
endfunction
function! s:still_have_issues() abort
let indentation = ' '
call health#report_info("If you're still having problems, " .
\ "try the following commands:\n" .
\ indentation . "$ export NVIM_PYTHON_LOG_FILE=/tmp/log\n" .
\ indentation . "$ export NVIM_PYTHON_LOG_LEVEL=DEBUG\n" .
\ indentation . "$ nvim\n" .
\ indentation . "$ cat /tmp/log_{PID}\n" .
\ indentation . ' and then create an issue on github'
\ )
endfunction
function! health#deoplete#check() abort
call health#report_start('deoplete.nvim')
call s:check_t_list()
call s:check_timers()
call s:check_required_python()
call s:check_required_msgpack()
call s:still_have_issues()
endfunction

View File

@ -0,0 +1,8 @@
coverage:
status:
project: false
patch: true
changes: true
comment:
layout: "diff"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
"=============================================================================
" FILE: deoplete.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================
if exists('g:loaded_deoplete')
finish
endif
let g:loaded_deoplete = 1
" Global options definition.
if get(g:, 'deoplete#enable_at_startup', 0)
call deoplete#enable()
endif

View File

@ -0,0 +1,72 @@
# ============================================================================
# FILE: __init__.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import typing
from importlib.util import find_spec
from deoplete.deoplete import Deoplete
from deoplete.util import Nvim
if find_spec('yarp'):
import vim
elif find_spec('pynvim'):
import pynvim as vim
else:
import neovim as vim
Context = typing.Dict[str, typing.Any]
if hasattr(vim, 'plugin'):
# Neovim only
@vim.plugin
class DeopleteHandlers(object):
def __init__(self, vim: Nvim):
self._vim = vim
@vim.function('_deoplete_init', sync=False) # type: ignore
def init_channel(self,
args: typing.List[typing.Any]) -> None:
self._deoplete = Deoplete(self._vim)
self._vim.call('deoplete#send_event', 'BufReadPost')
@vim.rpc_export('deoplete_enable_logging') # type: ignore
def enable_logging(self, context: Context) -> None:
self._deoplete.enable_logging()
@vim.rpc_export('deoplete_auto_completion_begin') # type: ignore
def auto_completion_begin(self, context: Context) -> None:
self._deoplete.completion_begin(context)
@vim.rpc_export('deoplete_manual_completion_begin') # type: ignore
def manual_completion_begin(self, context: Context) -> None:
self._deoplete.completion_begin(context)
@vim.rpc_export('deoplete_on_event') # type: ignore
def on_event(self, context: Context) -> None:
self._deoplete.on_event(context)
if find_spec('yarp'):
global_deoplete = Deoplete(vim)
def deoplete_init() -> None:
global_deoplete._vim.call('deoplete#send_event', 'BufReadPost')
def deoplete_enable_logging(context: Context) -> None:
global_deoplete.enable_logging()
def deoplete_auto_completion_begin(context: Context) -> None:
global_deoplete.completion_begin(context)
def deoplete_manual_completion_begin(context: Context) -> None:
global_deoplete.completion_begin(context)
def deoplete_on_event(context: Context) -> None:
global_deoplete.on_event(context)

View File

@ -0,0 +1,39 @@
# ============================================================================
# FILE: filter.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import typing
from abc import abstractmethod
from deoplete.logger import LoggingMixin
from deoplete.util import error_vim, Nvim, UserContext, Candidates
class Base(LoggingMixin):
def __init__(self, vim: Nvim) -> None:
self.vim = vim
self.name = 'base'
self.description = ''
self.vars: typing.Dict[str, typing.Any] = {}
def on_event(self, context: UserContext) -> None:
pass
def get_var(self, var_name: str) -> typing.Optional[typing.Any]:
custom_vars = self.vim.call(
'deoplete#custom#_get_filter', self.name)
if var_name in custom_vars:
return custom_vars[var_name]
if var_name in self.vars:
return self.vars[var_name]
return None
@abstractmethod
def filter(self, context: UserContext) -> Candidates:
return []
def print_error(self, expr: typing.Any) -> None:
error_vim(self.vim, expr)

View File

@ -0,0 +1,100 @@
# ============================================================================
# FILE: source.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
import typing
from abc import abstractmethod
from deoplete.logger import LoggingMixin
from deoplete.util import debug, error_vim, Nvim, UserContext, Candidates
class Base(LoggingMixin):
def __init__(self, vim: Nvim) -> None:
self.vim = vim
self.description = ''
self.mark = ''
self.name = ''
self.max_pattern_length = 80
self.min_pattern_length = -1
self.input_pattern = ''
self.input_patterns: typing.Dict[str, str] = {}
self.matchers = ['matcher_fuzzy']
self.sorters = ['sorter_rank']
self.converters = [
'converter_remove_overlap',
'converter_truncate_abbr',
'converter_truncate_kind',
'converter_truncate_info',
'converter_truncate_menu']
self.filetypes: typing.List[str] = []
self.keyword_patterns: typing.List[str] = []
self.is_debug_enabled = False
self.is_bytepos = False
self.is_initialized = False
self.is_volatile = False
self.is_async = False
self.is_silent = False
self.is_skip_langmap = True
self.rank = 100
self.disabled_syntaxes: typing.List[str] = []
self.events: typing.List[str] = []
self.vars: typing.Dict[str, typing.Any] = {}
self.max_abbr_width = 80
self.max_kind_width = 40
self.max_info_width = 200
self.max_menu_width = 40
self.max_candidates = 500
self.matcher_key = ''
self.dup = False
def get_complete_position(self, context: UserContext) -> int:
m = re.search('(?:' + context['keyword_pattern'] + ')$|$',
context['input'])
return m.start() if m else -1
def print(self, expr: typing.Any) -> None:
if not self.is_silent:
debug(self.vim, expr)
def print_error(self, expr: typing.Any) -> None:
if not self.is_silent:
error_vim(self.vim, expr)
@abstractmethod
def gather_candidates(self, context: UserContext) -> Candidates:
return []
def on_event(self, context: UserContext) -> None:
pass
def get_var(self, var_name: str) -> typing.Optional[typing.Any]:
custom_vars = self.vim.call(
'deoplete#custom#_get_source_vars', self.name)
if var_name in custom_vars:
return custom_vars[var_name]
if var_name in self.vars:
return self.vars[var_name]
return None
def get_filetype_var(self, filetype: str,
var_name: str) -> typing.Optional[typing.Any]:
var = self.get_var(var_name)
if var is None:
return None
ft = filetype if (filetype in var) else '_'
return var.get(ft, '')
def get_input_pattern(self, filetype: str) -> str:
if not self.input_patterns:
return self.input_pattern
ft = filetype if (filetype in self.input_patterns) else '_'
return self.input_patterns.get(ft, self.input_pattern)
def get_buf_option(self, option: str) -> typing.Any:
return self.vim.call('getbufvar', '%', '&' + option)

View File

@ -0,0 +1,604 @@
# ============================================================================
# FILE: child.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import copy
import os.path
import re
import sys
import time
import msgpack
import typing
from collections import defaultdict
from deoplete import logger
from deoplete.exceptions import SourceInitError
from deoplete.util import (bytepos2charpos, charpos2bytepos, error, error_tb,
import_plugin, get_custom, get_syn_names,
convert2candidates, uniq_list_dict, Nvim)
UserContext = typing.Dict[str, typing.Any]
Candidates = typing.Dict[str, typing.Any]
Result = typing.Dict[str, typing.Any]
class Child(logger.LoggingMixin):
def __init__(self, vim: Nvim) -> None:
self.name = 'child'
self._vim = vim
self._filters: typing.Dict[str, typing.Any] = {}
self._sources: typing.Dict[str, typing.Any] = {}
self._profile_flag = None
self._profile_start_time = 0
self._loaded_sources: typing.Dict[str, typing.Any] = {}
self._loaded_filters: typing.Dict[str, typing.Any] = {}
self._source_errors: typing.Dict[str, int] = defaultdict(int)
self._prev_results: typing.Dict[str, Result] = {}
if msgpack.version < (1, 0, 0):
self._packer = msgpack.Packer(
encoding='utf-8',
unicode_errors='surrogateescape')
self._unpacker = msgpack.Unpacker(
encoding='utf-8',
unicode_errors='surrogateescape')
else:
self._unpacker = msgpack.Unpacker(
unicode_errors='surrogateescape')
self._packer = msgpack.Packer(
unicode_errors='surrogateescape')
self._ignore_sources: typing.List[typing.Any] = []
def main_loop(self, stdout: typing.Any) -> None:
while True:
feed = sys.stdin.buffer.raw.read(102400) # type: ignore
if feed is None:
continue
if feed == b'':
# EOF
return
self._unpacker.feed(feed)
for child_in in self._unpacker:
name = child_in['name']
args = child_in['args']
queue_id = child_in['queue_id']
ret = self.main(name, args, queue_id)
if ret:
self._write(stdout, ret)
self._vim.call('deoplete#auto_complete', 'Update')
def main(self, name: str, args: typing.List[typing.Any],
queue_id: typing.Optional[int]) -> typing.Optional[Candidates]:
ret = None
if name == 'enable_logging':
self._enable_logging()
elif name == 'add_source':
self._add_source(args[0])
elif name == 'add_filter':
self._add_filter(args[0])
elif name == 'set_source_attributes':
self._set_source_attributes(args[0])
elif name == 'on_event':
self._on_event(args[0])
elif name == 'merge_results':
results = self._merge_results(args[0], queue_id)
if results['is_async'] or results['merged_results']:
ret = results
return ret
def _write(self, stdout: typing.Any, expr: typing.Any) -> None:
stdout.buffer.write(self._packer.pack(expr))
stdout.flush()
def _enable_logging(self) -> None:
logging = self._vim.vars['deoplete#_logging']
logger.setup(self._vim, logging['level'], logging['logfile'])
self.is_debug_enabled = True
def _add_source(self, path: str) -> None:
source = None
try:
Source = import_plugin(path, 'source', 'Source')
if not Source:
return
source = Source(self._vim)
name = os.path.splitext(os.path.basename(path))[0]
source.name = getattr(source, 'name', name)
source.path = path
if source.name in self._loaded_sources:
# Duplicated name
error_tb(self._vim, 'Duplicated source: %s' % source.name)
error_tb(self._vim, 'path: "%s" "%s"' %
(path, self._loaded_sources[source.name]))
source = None
except Exception:
error_tb(self._vim, 'Could not load source: %s' % path)
finally:
if source:
self._loaded_sources[source.name] = path
self._sources[source.name] = source
self.debug( # type: ignore
f'Loaded Source: {source.name} ({path})')
def _add_filter(self, path: str) -> None:
f = None
try:
Filter = import_plugin(path, 'filter', 'Filter')
if not Filter:
return
f = Filter(self._vim)
name = os.path.splitext(os.path.basename(path))[0]
f.name = getattr(f, 'name', name)
f.path = path
if f.name in self._loaded_filters:
# Duplicated name
error_tb(self._vim, 'Duplicated filter: %s' % f.name)
error_tb(self._vim, 'path: "%s" "%s"' %
(path, self._loaded_filters[f.name]))
f = None
except Exception:
# Exception occurred when loading a filter. Log stack trace.
error_tb(self._vim, 'Could not load filter: %s' % path)
finally:
if f:
self._loaded_filters[f.name] = path
self._filters[f.name] = f
self.debug( # type: ignore
f'Loaded Filter: {f.name} ({path})')
def _merge_results(self, context: UserContext,
queue_id: typing.Optional[int]) -> typing.Dict[
str, typing.Any]:
results = self._gather_results(context)
merged_results = []
for result in [x for x in results
if not self._is_skip(x['context'], x['source'])]:
candidates = self._get_candidates(
result, context['input'], context['next_input'])
if candidates:
rank = get_custom(context['custom'],
result['source'].name, 'rank',
result['source'].rank)
merged_results.append({
'complete_position': result['complete_position'],
'candidates': candidates,
'rank': rank,
})
is_async = len([x for x in results if x['is_async']]) > 0
return {
'queue_id': queue_id,
'is_async': is_async,
'merged_results': merged_results,
}
def _gather_results(self, context: UserContext) -> typing.List[Result]:
# Note: self._vim.current.buffer may not work when Vim quit
if context['changedtick'] != self._vim.eval('b:changedtick'):
return []
results = []
for source in [x[1] for x in self._itersource(context)]:
try:
result = self._get_result(context, source)
if not result:
continue
self._prev_results[source.name] = result
results.append(result)
except Exception as exc:
self._handle_source_exception(source, exc)
return results
def _get_result(self, context: UserContext,
source: typing.Any) -> Result:
if source.disabled_syntaxes and 'syntax_names' not in context:
context['syntax_names'] = get_syn_names(self._vim)
ctx = copy.deepcopy(context)
charpos = source.get_complete_position(ctx)
if charpos >= 0 and source.is_bytepos:
charpos = bytepos2charpos(
ctx['encoding'], ctx['input'], charpos)
ctx['char_position'] = charpos
ctx['complete_position'] = charpos2bytepos(
ctx['encoding'], ctx['input'], charpos)
ctx['complete_str'] = ctx['input'][ctx['char_position']:]
if charpos < 0 or self._is_skip(ctx, source):
if source.name in self._prev_results:
self._prev_results.pop(source.name)
# Skip
return {}
if (source.name in self._prev_results and
self._use_previous_result(
context, self._prev_results[source.name],
source.is_volatile, source.is_async)):
return self._prev_results[source.name]
ctx['is_async'] = False
ctx['is_refresh'] = True
ctx['max_abbr_width'] = min(source.max_abbr_width,
ctx['max_abbr_width'])
ctx['max_kind_width'] = min(source.max_kind_width,
ctx['max_kind_width'])
ctx['max_info_width'] = source.max_info_width
ctx['max_menu_width'] = min(source.max_menu_width,
ctx['max_menu_width'])
if ctx['max_abbr_width'] > 0:
ctx['max_abbr_width'] = max(20, ctx['max_abbr_width'])
if ctx['max_kind_width'] > 0:
ctx['max_kind_width'] = max(10, ctx['max_kind_width'])
if ctx['max_info_width'] > 0:
ctx['max_info_width'] = max(10, ctx['max_info_width'])
if ctx['max_menu_width'] > 0:
ctx['max_menu_width'] = max(10, ctx['max_menu_width'])
# Gathering
self._profile_start(ctx, source.name)
ctx['vars'] = self._vim.vars
ctx['candidates'] = source.gather_candidates(ctx)
if ctx['is_async']:
source.is_async = True
ctx['vars'] = None
self._profile_end(source.name)
if ctx['candidates'] is None:
return {}
ctx['candidates'] = convert2candidates(ctx['candidates'])
return {
'name': source.name,
'source': source,
'context': ctx,
'is_async': ctx['is_async'],
'prev_linenr': ctx['position'][1],
'prev_input': ctx['input'],
'input': ctx['input'],
'complete_position': ctx['complete_position'],
'candidates': ctx['candidates'],
}
def _gather_async_results(self, result: Result,
source: typing.Any) -> None:
try:
context = result['context']
context['is_refresh'] = False
context['vars'] = self._vim.vars
async_candidates = source.gather_candidates(context)
context['vars'] = None
result['is_async'] = context['is_async']
if async_candidates is None:
return
context['candidates'] += convert2candidates(async_candidates)
except Exception as exc:
self._handle_source_exception(source, exc)
def _handle_source_exception(self,
source: typing.Any, exc: Exception) -> None:
if isinstance(exc, SourceInitError):
error(self._vim,
f'Error when loading source {source.name}: {exc}. '
'Ignoring.')
self._ignore_sources.append(source.name)
return
self._source_errors[source.name] += 1
if source.is_silent:
return
if self._source_errors[source.name] > 2:
error(self._vim,
f'Too many errors from "{source.name}". '
'This source is disabled until Neovim is restarted.')
self._ignore_sources.append(source.name)
else:
error_tb(self._vim, f'Error from {source.name}: {exc}')
def _process_filter(self, f: typing.Any,
context: UserContext, max_candidates: int) -> None:
try:
self._profile_start(context, f.name)
if (isinstance(context['candidates'], dict) and
'sorted_candidates' in context['candidates']):
filtered: typing.List[typing.Any] = []
context['is_sorted'] = True
for candidates in context['candidates']['sorted_candidates']:
context['candidates'] = candidates
filtered += f.filter(context)
else:
filtered = f.filter(context)
if max_candidates > 0:
filtered = filtered[: max_candidates]
context['candidates'] = filtered
self._profile_end(f.name)
except Exception:
error_tb(self._vim, 'Errors from: %s' % f)
def _get_candidates(self, result: Result,
context_input: str, next_input: str
) -> typing.Optional[Candidates]:
source = result['source']
# Gather async results
if result['is_async']:
self._gather_async_results(result, source)
if not result['candidates']:
return None
# Source context
ctx = copy.copy(result['context'])
ctx['input'] = context_input
ctx['next_input'] = next_input
ctx['complete_str'] = context_input[ctx['char_position']:]
ctx['is_sorted'] = False
# Set ignorecase
case = ctx['smartcase'] or ctx['camelcase']
if case:
if re.search(r'[A-Z]', ctx['complete_str']):
ctx['ignorecase'] = False
else:
ctx['ignorecase'] = True
ignorecase = ctx['ignorecase']
# Match
matchers = [self._filters[x] for x
in source.matchers if x in self._filters]
if source.matcher_key != '':
original_candidates = ctx['candidates']
# Convert word key to matcher_key
for candidate in original_candidates:
candidate['__save_word'] = candidate['word']
candidate['word'] = candidate[source.matcher_key]
for f in matchers:
self._process_filter(f, ctx, source.max_candidates)
if source.matcher_key != '':
# Restore word key
for candidate in original_candidates:
candidate['word'] = candidate['__save_word']
del candidate['__save_word']
# Sort
sorters = [self._filters[x] for x
in source.sorters if x in self._filters]
for f in sorters:
self._process_filter(f, ctx, source.max_candidates)
# Note: converter may break candidates
ctx['candidates'] = copy.deepcopy(ctx['candidates'])
# Convert
converters = [self._filters[x] for x
in source.converters if x in self._filters]
for f in converters:
self._process_filter(f, ctx, source.max_candidates)
if (isinstance(ctx['candidates'], dict) and
'sorted_candidates' in ctx['candidates']):
sorted_candidates = ctx['candidates']['sorted_candidates']
ctx['candidates'] = []
for candidates in sorted_candidates:
ctx['candidates'] += candidates
ctx['ignorecase'] = ignorecase
# On post filter
if hasattr(source, 'on_post_filter'):
ctx['candidates'] = source.on_post_filter(ctx)
mark = source.mark + ' '
refresh = False
refresh_always = self._vim.call(
'deoplete#custom#_get_option', 'refresh_always')
auto_complete = self._vim.call(
'deoplete#custom#_get_option', 'auto_complete')
eskk_check = self._vim.call(
'deoplete#util#check_eskk_phase_henkan')
if refresh_always and auto_complete and not eskk_check:
refresh = True
for candidate in ctx['candidates']:
candidate['icase'] = 1
candidate['equal'] = refresh
# Set default menu
if (mark != ' ' and
candidate.get('menu', '').find(mark) != 0):
candidate['menu'] = mark + candidate.get('menu', '')
if source.dup:
candidate['dup'] = 1
# Note: cannot use set() for dict
if source.dup:
# Remove duplicates
ctx['candidates'] = uniq_list_dict(ctx['candidates'])
return ctx['candidates'] # type: ignore
def _itersource(self, context: UserContext
) -> typing.Generator[typing.Any, None, None]:
filetypes = context['filetypes']
ignore_sources = set(self._ignore_sources)
for ft in filetypes:
ignore_sources.update(
self._vim.call('deoplete#custom#_get_filetype_option',
'ignore_sources', ft, []))
for source_name, source in self._get_sources().items():
if source.filetypes is None or source_name in ignore_sources:
continue
if context['sources'] and source_name not in context['sources']:
continue
if source.filetypes and not any(x in filetypes
for x in source.filetypes):
continue
if not source.is_initialized and hasattr(source, 'on_init'):
self.debug('on_init Source: ' + source.name) # type: ignore
try:
context['vars'] = self._vim.vars
source.on_init(context)
context['vars'] = None
except Exception as exc:
if isinstance(exc, SourceInitError):
error(self._vim, 'Error when loading source '
f'{source_name}: {exc}. Ignoring.')
else:
error_tb(self._vim, 'Error when loading source '
f'{source_name}: {exc}. Ignoring.')
self._ignore_sources.append(source_name)
continue
else:
source.is_initialized = True
yield source_name, source
def _profile_start(self, context: UserContext, name: str) -> None:
if self._profile_flag == 0 or not self.is_debug_enabled:
return
if not self._profile_flag:
self._profile_flag = self._vim.call(
'deoplete#custom#_get_option', 'profile')
if self._profile_flag:
return self._profile_start(context, name)
elif self._profile_flag:
self.debug(f'Profile Start: {name}')
self._profile_start_time = time.monotonic()
def _profile_end(self, name: str) -> None:
if not self._profile_start_time:
return
self.debug( # type: ignore
'Profile End : {0:<25} time={1:2.10f}'.format(
name, time.monotonic() - self._profile_start_time))
def _use_previous_result(self, context: UserContext,
result: Result, is_volatile: bool,
is_async: bool) -> bool:
if context['position'][1] != result['prev_linenr']:
return False
elif is_async:
# Note: If it is async, the cache must be used to call
# gather_async_results().
return bool(context['input'] == result['prev_input'])
elif is_volatile:
# Note: If it is volatile, the cache must be disabled to refresh
# candidates.
return False
else:
return bool(re.sub(r'\w*$', '', context['input']) ==
re.sub(r'\w*$', '', result['prev_input']) and
context['input'].find(result['prev_input']) == 0)
def _is_skip(self, context: UserContext, source: typing.Any) -> bool:
if 'syntax_names' in context and source.disabled_syntaxes:
p = re.compile('(' + '|'.join(source.disabled_syntaxes) + ')$')
if next(filter(p.search, context['syntax_names']), None):
return True
iminsert = self._vim.call('getbufvar', '%', '&iminsert')
if iminsert == 1 and source.is_skip_langmap:
return True
for ft in context['filetypes']:
input_pattern = source.get_input_pattern(ft)
if (input_pattern != '' and
re.search('(' + input_pattern + ')$', context['input'])):
return False
auto_complete_popup = self._vim.call(
'deoplete#custom#_get_option', 'auto_complete_popup')
if context['event'] == 'Manual' or auto_complete_popup == 'manual':
return False
return not (source.min_pattern_length <=
len(context['complete_str']) <= source.max_pattern_length)
def _set_source_attributes(self, context: UserContext) -> None:
"""Set source attributes from the context.
Each item in `attrs` is the attribute name.
"""
attrs = (
'converters',
'disabled_syntaxes',
'dup',
'filetypes',
'input_pattern',
'is_debug_enabled',
'is_silent',
'is_volatile',
'mark',
'matchers',
'max_abbr_width',
'max_candidates',
'max_info_width',
'max_kind_width',
'max_menu_width',
'max_pattern_length',
'min_pattern_length',
'sorters',
)
for name, source in self._get_sources().items():
self.debug('Set Source attributes: %s', name) # type: ignore
source.dup = bool(source.filetypes)
for attr in attrs:
source_attr = getattr(source, attr, None)
custom = get_custom(context['custom'],
name, attr, source_attr)
if type(getattr(source, attr)) != type(custom):
# Type check
error(self._vim, f'source {source.name}: '
f'custom attr "{attr}" is wrong type.')
elif custom and isinstance(source_attr, dict):
# Update values if it is dict
source_attr.update(custom)
else:
setattr(source, attr, custom)
self.debug('Attribute: %s (%s)', # type: ignore
attr, getattr(source, attr))
# Default min_pattern_length
if source.min_pattern_length < 0:
source.min_pattern_length = self._vim.call(
'deoplete#custom#_get_option', 'min_pattern_length')
def _on_event(self, context: UserContext) -> None:
event = context['event']
context['vars'] = self._vim.vars
for source_name, source in self._itersource(context):
if not source.events or event in source.events:
try:
source.on_event(context)
except Exception as exc:
error_tb(self._vim,
f'Exception during {source.name}.on_event '
'for event {!r}: {}'.format(event, exc))
for f in self._filters.values():
f.on_event(context)
context['vars'] = None
def _get_sources(self) -> typing.Dict[str, typing.Any]:
# Note: for the size change of "self._sources" error
return copy.copy(self._sources)

View File

@ -0,0 +1,150 @@
# ============================================================================
# FILE: context.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import os
import re
import typing
from deoplete.util import Nvim
UserContext = typing.Dict[str, typing.Any]
class Context(object):
def __init__(self, vim: Nvim) -> None:
self._vim = vim
self._prev_filetype = ''
self._cached: typing.Optional[UserContext] = None
self._cached_filetype = self._init_cached_filetype(
self._prev_filetype)
self._init_cached()
self._context_filetype: UserContext = {}
def get(self, event: str) -> UserContext:
text = self._vim.call('deoplete#util#get_input', event)
[filetype, filetypes, same_filetypes] = self._get_context_filetype(
text, event, self._vim.call('getbufvar', '%', '&filetype'))
m = re.search(r'\w$', text)
word_len = len(m.group(0)) if m else 0
max_width = self._vim.call('winwidth', 0) - self._vim.call('col', '.')
max_width += word_len
context: UserContext = {
'changedtick': self._vim.call(
'getbufvar', '%', 'changedtick', 0),
'event': event,
'filetype': filetype,
'filetypes': filetypes,
'input': text,
'max_abbr_width': max_width,
'max_kind_width': max_width,
'max_menu_width': max_width,
'next_input': self._vim.call(
'deoplete#util#get_next_input', event),
'position': self._vim.call('getpos', '.'),
'same_filetypes': same_filetypes,
}
context.update(self._cached) # type: ignore
if filetype != self._prev_filetype:
self._prev_filetype = filetype
self._cached_filetype = self._init_cached_filetype(filetype)
context.update(self._cached_filetype)
return context
def _init_cached_filetype(self, filetype: str) -> UserContext:
return {
'keyword_pattern': self._vim.call(
'deoplete#util#get_keyword_pattern', filetype),
'sources': self._vim.call(
'deoplete#custom#_get_filetype_option',
'sources', filetype, []),
}
def _init_cached(self) -> None:
bufnr = self._vim.call('expand', '<abuf>')
if not bufnr:
bufnr = self._vim.call('bufnr', '%')
if not bufnr:
bufnr = -1
bufname = ''
else:
bufname = self._vim.call('bufname', bufnr)
cwd = self._vim.call('getcwd')
buftype = self._vim.call('getbufvar', '%', '&buftype')
bufpath = (bufname if os.path.isabs(bufname)
else os.path.join(cwd, bufname))
if not os.path.exists(bufpath) or 'nofile' in buftype:
bufpath = ''
self._cached = {
'bufnr': bufnr,
'bufname': bufname,
'bufpath': bufpath,
'camelcase': self._vim.call(
'deoplete#custom#_get_option', 'camel_case'),
'complete_str': '',
'custom': self._vim.call('deoplete#custom#_get'),
'cwd': cwd,
'encoding': self._vim.options['encoding'],
'ignorecase': self._vim.call(
'deoplete#custom#_get_option', 'ignore_case'),
'is_windows': self._vim.call('has', 'win32'),
'smartcase': self._vim.call(
'deoplete#custom#_get_option', 'smart_case'),
}
def _get_context_filetype(self,
text: str, event: str, filetype: str
) -> typing.List[typing.Any]:
if not self._context_filetype and self._vim.call(
'exists', '*context_filetype#get_filetype'):
# Force context_filetype call
self._vim.call('context_filetype#get_filetype')
linenr = self._vim.call('line', '.')
bufnr = self._vim.call('bufnr', '%')
if (not self._context_filetype or
self._context_filetype['prev_filetype'] != filetype or
self._context_filetype['line'] != linenr or
self._context_filetype['bufnr'] != bufnr or
re.sub(r'\w+$', '', self._context_filetype['input']) !=
re.sub(r'\w+$', '', self._context_filetype['input']) or
event == 'InsertEnter'):
self._cache_context_filetype(text, filetype, linenr, bufnr)
return [
self._context_filetype['filetype'],
self._context_filetype['filetypes'],
self._context_filetype['same_filetypes']
]
def _cache_context_filetype(self, text: str, filetype: str,
linenr: int, bufnr: int) -> None:
exists_context_filetype = self._vim.call(
'exists', '*context_filetype#get_filetype')
self._context_filetype = {
'line': linenr,
'bufnr': bufnr,
'input': text,
'prev_filetype': filetype,
'filetype': (
self._vim.call('context_filetype#get_filetype')
if exists_context_filetype
else (filetype if filetype else 'nothing')),
'filetypes': (
self._vim.call('context_filetype#get_filetypes')
if exists_context_filetype
else filetype.split('.')),
'same_filetypes': (
self._vim.call('context_filetype#get_same_filetypes')
if exists_context_filetype else []),
}

View File

@ -0,0 +1,288 @@
# ============================================================================
# FILE: deoplete.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import copy
import glob
import os
import typing
import deoplete.parent
from deoplete import logger
from deoplete.context import Context
from deoplete.util import error, error_tb, Nvim
UserContext = typing.Dict[str, typing.Any]
Candidates = typing.Dict[str, typing.Any]
Parent = typing.Union[deoplete.parent.SyncParent, deoplete.parent.AsyncParent]
class Deoplete(logger.LoggingMixin):
def __init__(self, vim: Nvim):
self.name = 'core'
self._vim = vim
self._runtimepath = ''
self._runtimepath_list: typing.List[str] = []
self._custom: typing.Dict[str, typing.Dict[str, typing.Any]] = {}
self._loaded_paths: typing.Set[str] = set()
self._prev_results: typing.Dict[int, Candidates] = {}
self._prev_input = ''
self._prev_next_input = ''
self._context: typing.Optional[Context] = None
self._parents: typing.List[Parent] = []
self._parent_count = 0
self._max_parents = self._vim.call('deoplete#custom#_get_option',
'num_processes')
if self._max_parents != 1 and not hasattr(self._vim, 'loop'):
msg = ('pynvim 0.3.0+ is required for %d parents. '
'Using single process.' % self._max_parents)
error(self._vim, msg)
self._max_parents = 1
# Enable logging for more information, and e.g.
# deoplete-jedi picks up the log filename from deoplete's handler in
# its on_init.
if self._vim.vars['deoplete#_logging']:
self.enable_logging()
if hasattr(self._vim, 'channel_id'):
self._vim.vars['deoplete#_channel_id'] = self._vim.channel_id
self._vim.vars['deoplete#_initialized'] = True
def enable_logging(self) -> None:
logging = self._vim.vars['deoplete#_logging']
logger.setup(self._vim, logging['level'], logging['logfile'])
self.is_debug_enabled = True
def init_context(self) -> None:
self._context = Context(self._vim)
# Initialization
context = self._context.get('Init')
context['rpc'] = 'deoplete_on_event'
self.on_event(context)
def completion_begin(self, user_context: UserContext) -> None:
if not self._context:
self.init_context()
context = self._context.get(user_context['event']) # type: ignore
context.update(user_context)
self.debug('completion_begin (%s): %r', # type: ignore
context['event'], context['input'])
if self._vim.call('deoplete#handler#_check_omnifunc', context):
return
self._check_recache(context)
try:
(is_async, needs_poll,
position, candidates) = self._merge_results(context)
except Exception:
error_tb(self._vim, 'Error while gathering completions')
is_async = False
needs_poll = False
position = -1
candidates = []
if needs_poll:
self._vim.call('deoplete#handler#_async_timer_start')
if not candidates:
self._vim.call('deoplete#mapping#_restore_completeopt')
# Async update is skipped if same.
prev_completion = self._vim.vars['deoplete#_prev_completion']
prev_candidates = prev_completion['candidates']
event = context['event']
if (event == 'Async' or event == 'Update' and
prev_candidates and candidates == prev_candidates):
return
# error(self._vim, candidates)
self._vim.vars['deoplete#_context'] = {
'complete_position': position,
'candidates': candidates,
'event': context['event'],
'input': context['input'],
'is_async': is_async,
}
if candidates or self._vim.call('deoplete#util#check_popup'):
self.debug('do_complete (%s): ' # type: ignore
+ '%d candidates, input=%s, complete_position=%d, '
+ 'is_async=%d',
context['event'],
len(candidates), context['input'], position,
is_async)
self._vim.call('deoplete#handler#_do_complete')
def on_event(self, user_context: UserContext) -> None:
self._vim.call('deoplete#custom#_update_cache')
if not self._context:
self.init_context()
else:
self._context._init_cached()
context = self._context.get(user_context['event']) # type: ignore
context.update(user_context)
self.debug('initialized context: %s', context) # type: ignore
self.debug('on_event: %s', context['event']) # type: ignore
self._check_recache(context)
for parent in self._parents:
parent.on_event(context)
def _get_results(self, context: UserContext) -> typing.List[typing.Any]:
is_async = False
needs_poll = False
results: typing.List[Candidates] = []
for cnt, parent in enumerate(self._parents):
if cnt in self._prev_results:
# Use previous result
results += copy.deepcopy(
self._prev_results[cnt]) # type: ignore
else:
result = parent.merge_results(context)
is_async = is_async or result[0]
needs_poll = needs_poll or result[1]
if not result[0]:
self._prev_results[cnt] = result[2]
results += result[2]
return [is_async, needs_poll, results]
def _merge_results(self, context: UserContext) -> typing.Tuple[
bool, bool, int, typing.List[typing.Any]]:
use_prev = (context['input'] == self._prev_input
and context['next_input'] == self._prev_next_input
and context['event'] != 'Manual')
if not use_prev:
self._prev_results = {}
self._prev_input = context['input']
self._prev_next_input = context['next_input']
[is_async, needs_poll, results] = self._get_results(context)
if not results:
return (is_async, needs_poll, -1, [])
complete_position = min(x['complete_position'] for x in results)
all_candidates: typing.List[Candidates] = []
for result in sorted(results, key=lambda x: x['rank'], reverse=True):
candidates = result['candidates']
prefix = context['input'][
complete_position:result['complete_position']]
if prefix != '':
for candidate in candidates:
# Add prefix
candidate['word'] = prefix + candidate['word']
all_candidates += candidates
# self.debug(candidates)
max_list = self._vim.call(
'deoplete#custom#_get_option', 'max_list')
if max_list > 0:
all_candidates = all_candidates[: max_list]
candidate_marks = self._vim.call(
'deoplete#custom#_get_option', 'candidate_marks')
if candidate_marks:
all_candidates = copy.deepcopy(all_candidates)
for i, candidate in enumerate(all_candidates):
mark = (candidate_marks[i] if i < len(candidate_marks) and
candidate_marks[i] else ' ')
candidate['menu'] = mark + ' ' + candidate.get('menu', '')
return (is_async, needs_poll, complete_position, all_candidates)
def _add_parent(self, parent_cls: typing.Callable[
[Nvim], Parent]) -> None:
parent = parent_cls(self._vim)
if self._vim.vars['deoplete#_logging']:
parent.enable_logging()
self._parents.append(parent)
def _find_rplugins(self,
source: str) -> typing.Generator[str, None, None]:
"""Search for base.py or *.py
Searches $VIMRUNTIME/*/rplugin/python3/deoplete/$source[s]/
"""
if not self._runtimepath_list:
return
sources = (
os.path.join('rplugin', 'python3', 'deoplete',
source, '*.py'),
os.path.join('rplugin', 'python3', 'deoplete',
source + 's', '*.py'),
os.path.join('rplugin', 'python3', 'deoplete',
source, '*', '*.py'),
)
for src in sources:
for path in self._runtimepath_list:
yield from glob.iglob(os.path.join(path, src))
def _load_sources(self, context: UserContext) -> None:
if not self._parents and self._max_parents == 1:
self._add_parent(deoplete.parent.SyncParent)
for path in self._find_rplugins('source'):
if (path in self._loaded_paths
or os.path.basename(path) == 'base.py'):
continue
self._loaded_paths.add(path)
if len(self._parents) <= self._parent_count:
# Add parent automatically
self._add_parent(deoplete.parent.AsyncParent)
self._parents[self._parent_count].add_source(path)
self.debug( # type: ignore
f'Process {self._parent_count}: {path}')
self._parent_count += 1
if self._max_parents > 0:
self._parent_count %= self._max_parents
self._set_source_attributes(context)
def _load_filters(self, context: UserContext) -> None:
for path in self._find_rplugins('filter'):
for parent in self._parents:
parent.add_filter(path)
def _set_source_attributes(self, context: UserContext) -> None:
for parent in self._parents:
parent.set_source_attributes(context)
def _check_recache(self, context: UserContext) -> None:
runtimepath = self._vim.options['runtimepath']
if runtimepath != self._runtimepath:
self._runtimepath = runtimepath
self._runtimepath_list = runtimepath.split(',')
self._load_sources(context)
self._load_filters(context)
if context['rpc'] != 'deoplete_on_event':
self.on_event(context)
elif context['custom'] != self._custom:
self._set_source_attributes(context)
self._custom = context['custom']

View File

@ -0,0 +1,6 @@
class SourceInitError(Exception):
"""Error during source initialization.
This can be used to have a clearer message, where not traceback gets
displayed.
"""

View File

@ -0,0 +1,14 @@
# ============================================================================
# FILE: base.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
# For backward compatibility
from deoplete.base.filter import Base as _Base
from deoplete.util import Nvim
class Base(_Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)

View File

@ -0,0 +1,40 @@
# ============================================================================
# FILE: converter_auto_delimiter.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import typing
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_auto_delimiter'
self.description = 'auto delimiter converter'
self.vars = {
'delimiters': ['/'],
}
def filter(self, context: UserContext) -> Candidates:
delimiters: typing.List[str] = self.get_var( # type: ignore
'delimiters')
for candidate, delimiter in [
[x, last_find(x['abbr'], delimiters)]
for x in context['candidates']
if 'abbr' in x and x['abbr'] and
not last_find(x['word'], delimiters) and
last_find(x['abbr'], delimiters)]:
candidate['word'] += delimiter
return context['candidates'] # type: ignore
def last_find(s: str, needles: typing.List[str]) -> typing.Optional[str]:
for needle in needles:
if len(s) >= len(needle) and s[-len(needle):] == needle:
return needle
return None

View File

@ -0,0 +1,29 @@
# ============================================================================
# FILE: converter_auto_paren.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_auto_paren'
self.description = 'auto add parentheses converter'
def filter(self, context: UserContext) -> Candidates:
p1 = re.compile(r'\(\)?$')
p2 = re.compile(r'\(.*\)')
for candidate in [
x for x in context['candidates']
if not p1.search(x['word']) and
(('abbr' in x and p2.search(x['abbr'])) or
('info' in x and p2.search(x['info'])))]:
candidate['word'] += '('
return context['candidates'] # type: ignore

View File

@ -0,0 +1,32 @@
# ============================================================================
# FILE: converter_case.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_case'
self.description = 'case converter'
def filter(self, context: UserContext) -> Candidates:
complete_str = context['complete_str']
if not re.search(r'[A-Z]', complete_str):
return context['candidates'] # type: ignore
complete_lower = complete_str.lower()
complete_len = len(complete_str)
for candidate in [
x for x in context['candidates']
if x['word'].lower().startswith(complete_lower)]:
candidate['word'] = complete_str + candidate[
'word'][complete_len:]
return context['candidates'] # type: ignore

View File

@ -0,0 +1,49 @@
# ============================================================================
# FILE: converter_remove_overlap.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
import typing
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_remove_overlap'
self.description = 'remove overlap converter'
def filter(self, context: UserContext) -> Candidates:
if not context['next_input']:
return context['candidates'] # type: ignore
next_input_words = [x for x in re.split(
r'([a-zA-Z_]+|\W)', context['next_input']) if x]
check_pairs = []
if self.vim.call('searchpair', '(', '', ')', 'bnw'):
check_pairs.append(['(', ')'])
if self.vim.call('searchpair', '[', '', ']', 'bnw'):
check_pairs.append(['[', ']'])
for [overlap, candidate, word] in [
[x, y, y['word']] for x, y
in [[overlap_length(x['word'], next_input_words), x]
for x in context['candidates']] if x > 0]:
if [x for x in check_pairs if x[0] in word and x[1] in word]:
continue
if 'abbr' not in candidate:
candidate['abbr'] = word
candidate['word'] = word[: -overlap]
return context['candidates'] # type: ignore
def overlap_length(left: str, next_input_words: typing.List[str]) -> int:
pos = len(next_input_words)
while pos > 0 and not left.endswith(''.join(next_input_words[:pos])):
pos -= 1
return len(''.join(next_input_words[:pos]))

View File

@ -0,0 +1,25 @@
# ============================================================================
# FILE: converter_remove_paren.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_remove_paren'
self.description = 'remove parentheses converter'
def filter(self, context: UserContext) -> Candidates:
for candidate in [x for x in context['candidates']
if '(' in x['word']]:
candidate['word'] = re.sub(r'\(.*\)(\$\d+)?', '',
candidate['word'])
return context['candidates'] # type: ignore

View File

@ -0,0 +1,77 @@
# ============================================================================
# FILE: converter_reorder_attr.py
# AUTHOR: @reaysawa
# License: MIT license
# ============================================================================
import re
import typing
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_reorder_attr'
self.description = 'Reorder candidates based on their attributes'
self.vars = {
'attrs_order': {},
}
@staticmethod
def filter_attrs(candidates: Candidates,
preferred_order_attrs: typing.Dict[str, typing.Any],
max_list_size: int = 500) -> Candidates:
context_candidates = candidates[:]
new_candidates = []
new_candidates_len = 0
for attr in preferred_order_attrs.keys():
for expr in preferred_order_attrs[attr]:
disabled = expr[0] == '!'
if disabled:
expr = expr[1:]
expr = re.compile(expr)
size = len(context_candidates)
i = 0
while i < size:
candidate = context_candidates[i]
if attr in candidate and expr.search(candidate[attr]):
candidate = context_candidates.pop(i)
# Popping will make 'i' effectively go forward an extra
# time; because of that, decrease for now and wait for
# the +1 at the bottom to balance that out.
i -= 1
size -= 1
if not disabled:
new_candidates.append(candidate)
new_candidates_len += 1
# Stop filtering if the maximum has been achieved
if new_candidates_len == max_list_size:
return new_candidates
i += 1
# Add remaining at the bottom
new_candidates.extend(context_candidates)
# Go to the next attribute with the new list order
context_candidates = new_candidates
return new_candidates
def filter(self, context: UserContext) -> Candidates:
preferred_order_attrs = self.get_var( # type: ignore
'attrs_order').get(context['filetype'], [])
if not context['candidates'] or not preferred_order_attrs:
return context['candidates'] # type: ignore
max_list_size = self.vim.call(
'deoplete#custom#_get_option', 'max_list'
)
return self.filter_attrs(
context['candidates'], preferred_order_attrs, max_list_size
)

View File

@ -0,0 +1,28 @@
# ============================================================================
# FILE: converter_truncate_abbr.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import truncate_skipping, Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_truncate_abbr'
self.description = 'truncate abbr converter'
def filter(self, context: UserContext) -> Candidates:
max_width = context['max_abbr_width']
if max_width <= 0:
return context['candidates'] # type: ignore
footer_width = max_width / 3
for candidate in context['candidates']:
candidate['abbr'] = truncate_skipping(
candidate.get('abbr', candidate['word']),
max_width, '..', footer_width)
return context['candidates'] # type: ignore

View File

@ -0,0 +1,28 @@
# ============================================================================
# FILE: converter_truncate_info.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import truncate_skipping, Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_truncate_info'
self.description = 'truncate info converter'
def filter(self, context: UserContext) -> Candidates:
max_width = context['max_info_width']
if not context['candidates'] or max_width <= 0:
return context['candidates'] # type: ignore
footer_width = 1
for candidate in context['candidates']:
candidate['info'] = truncate_skipping(
candidate.get('info', ''),
max_width, '..', footer_width)
return context['candidates'] # type: ignore

View File

@ -0,0 +1,29 @@
# ============================================================================
# FILE: converter_truncate_kind.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import truncate_skipping, Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_truncate_kind'
self.description = 'truncate kind converter'
def filter(self, context: UserContext) -> Candidates:
max_width = context['max_kind_width']
if not context['candidates'] or 'kind' not in context[
'candidates'][0] or max_width <= 0:
return context['candidates'] # type: ignore
footer_width = max_width / 3
for candidate in context['candidates']:
candidate['kind'] = truncate_skipping(
candidate.get('kind', ''),
max_width, '..', footer_width)
return context['candidates'] # type: ignore

View File

@ -0,0 +1,29 @@
# ============================================================================
# FILE: converter_truncate_menu.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import truncate_skipping, Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'converter_truncate_menu'
self.description = 'truncate menu converter'
def filter(self, context: UserContext) -> Candidates:
max_width = context['max_menu_width']
if not context['candidates'] or 'menu' not in context[
'candidates'][0] or max_width <= 0:
return context['candidates'] # type: ignore
footer_width = max_width / 3
for candidate in context['candidates']:
candidate['menu'] = truncate_skipping(
candidate.get('menu', ''),
max_width, '..', footer_width)
return context['candidates'] # type: ignore

View File

@ -0,0 +1,76 @@
# ============================================================================
# FILE: matcher_cpsm.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import os
import sys
import typing
from deoplete.base.filter import Base
from deoplete.util import error, globruntime
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'matcher_cpsm'
self.description = 'cpsm matcher'
self._cpsm: typing.Optional[typing.Any] = None
def filter(self, context: UserContext) -> Candidates:
if (not context['candidates'] or not context['input']
or self._cpsm is False):
return context['candidates'] # type: ignore
if self._cpsm is None:
errmsg = self._init_cpsm(context)
if errmsg:
error(self.vim, 'matcher_cpsm: %s' % errmsg)
return []
complete_str = context['complete_str']
if context['ignorecase']:
complete_str = complete_str.lower()
cpsm_result = self._get_cpsm_result(
context['candidates'], complete_str)
return [x for x in context['candidates']
if x['word'] in sorted(cpsm_result, key=cpsm_result.index)]
def _init_cpsm(self, context: UserContext) -> str:
ext = '.pyd' if context['is_windows'] else '.so'
fname = 'bin/cpsm_py' + ext
found = globruntime(self.vim.options['runtimepath'], fname)
errmsg = ''
if found:
sys.path.insert(0, os.path.dirname(found[0]))
try:
import cpsm_py
except ImportError as exc:
import traceback
errmsg = 'Could not import cpsm_py: %s\n%s' % (
exc, traceback.format_exc())
else:
self._cpsm = cpsm_py
finally:
sys.path.pop(0)
else:
errmsg = (
'%s was not found in runtimepath. '
'You must install/build cpsm with Python 3 support.' % (
fname))
if errmsg:
self._cpsm = False
return errmsg
def _get_cpsm_result(self, candidates: Candidates,
pattern: str) -> typing.List[str]:
return self._cpsm.ctrlp_match( # type: ignore
(d['word'] for d in candidates),
pattern, limit=1000, ispath=False)[0]

View File

@ -0,0 +1,30 @@
# ============================================================================
# FILE: matcher_full_fuzzy.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.filter import Base
from deoplete.util import fuzzy_escape, Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'matcher_full_fuzzy'
self.description = 'full fuzzy matcher'
def filter(self, context: UserContext) -> Candidates:
complete_str = context['complete_str']
if context['ignorecase']:
complete_str = complete_str.lower()
p = re.compile(fuzzy_escape(complete_str, context['camelcase']))
if context['ignorecase']:
return [x for x in context['candidates']
if p.search(x['word'].lower())]
else:
return [x for x in context['candidates']
if p.search(x['word'])]

View File

@ -0,0 +1,45 @@
# ============================================================================
# FILE: matcher_fuzzy.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.filter import Base
from deoplete.util import (
fuzzy_escape, binary_search_begin, binary_search_end)
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'matcher_fuzzy'
self.description = 'fuzzy matcher'
def filter(self, context: UserContext) -> Candidates:
complete_str = context['complete_str']
if context['ignorecase']:
complete_str = complete_str.lower()
if not complete_str:
return context['candidates'] # type: ignore
if context['is_sorted']:
begin = binary_search_begin(
context['candidates'], complete_str[0])
end = binary_search_end(
context['candidates'], complete_str[0])
if begin < 0 or end < 0:
return []
candidates = context['candidates'][begin:end+1]
else:
candidates = context['candidates']
p = re.compile(fuzzy_escape(complete_str, context['camelcase']))
if context['ignorecase']:
return [x for x in candidates if p.match(x['word'].lower())]
else:
return [x for x in candidates if p.match(x['word'])]

View File

@ -0,0 +1,44 @@
# ============================================================================
# FILE: matcher_head.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import binary_search_begin, binary_search_end
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'matcher_head'
self.description = 'head matcher'
def filter(self, context: UserContext) -> Candidates:
complete_str = context['complete_str']
if context['ignorecase']:
complete_str = complete_str.lower()
if context['is_sorted']:
begin = binary_search_begin(
context['candidates'], complete_str)
end = binary_search_end(
context['candidates'], complete_str)
if begin < 0 or end < 0:
return []
candidates = context['candidates'][begin:end+1]
if context['ignorecase']:
return candidates # type: ignore
else:
candidates = context['candidates']
if context['ignorecase']:
return [x for x in context['candidates']
if x['word'].lower().startswith(complete_str)]
else:
return [x for x in context['candidates']
if x['word'].startswith(complete_str)]

View File

@ -0,0 +1,22 @@
# ============================================================================
# FILE: matcher_length.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'matcher_length'
self.description = 'length matcher'
def filter(self, context: UserContext) -> Candidates:
input_len = len(context['complete_str'])
return [x for x in context['candidates']
if len(x['word']) > input_len]

View File

@ -0,0 +1,52 @@
# ============================================================================
# FILE: sorter_rank.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
import typing
from deoplete.base.filter import Base
from deoplete.util import getlines
from deoplete.util import Nvim, UserContext, Candidates, Candidate
LINES_MAX = 150
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'sorter_rank'
self.description = 'rank sorter'
self._cache: typing.Dict[str, typing.Set[int]] = {}
def on_event(self, context: UserContext) -> None:
self._cache = {}
start = max([1, context['position'][1] - LINES_MAX])
linenr = start
for line in getlines(self.vim, start, start + LINES_MAX):
for m in re.finditer(context['keyword_pattern'], line):
k = m.group(0)
if k not in self._cache:
self._cache[k] = set()
self._cache[k].add(linenr)
linenr += 1
def filter(self, context: UserContext) -> Candidates:
complete_str = context['complete_str'].lower()
linenr = context['position'][1]
def compare(x: Candidate) -> int:
word = x['word']
matched = int(complete_str in word.lower())
score = -matched * 40
if word in self._cache:
mru = min([abs(x - linenr) for x in self._cache[word]])
mru -= LINES_MAX
score += mru * 10
return score
return sorted(context['candidates'], key=compare)

View File

@ -0,0 +1,21 @@
# ============================================================================
# FILE: sorter_word.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete.base.filter import Base
from deoplete.util import Nvim, UserContext, Candidates
class Filter(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'sorter_word'
self.description = 'word sorter'
def filter(self, context: UserContext) -> Candidates:
return sorted(context['candidates'],
key=lambda x: x['word'].swapcase())

View File

@ -0,0 +1,166 @@
# ============================================================================
# FILE: logger.py
# AUTHOR: Tommy Allen <tommy at esdf.io>
# License: MIT license
# ============================================================================
import sys
import time
import logging
import typing
from deoplete.util import Nvim
from functools import wraps
from collections import defaultdict
log_format = '%(asctime)s %(levelname)-8s [%(process)d] (%(name)s) %(message)s'
log_message_cooldown = 0.5
root = logging.getLogger('deoplete')
root.propagate = False
init = False
FUNC = typing.Callable[..., typing.Any]
def getLogger(name: str) -> logging.Logger:
"""Get a logger that is a child of the 'root' logger.
"""
return root.getChild(name)
def setup(vim: Nvim, level: str, output_file: str = '') -> None:
"""Setup logging for Deoplete
"""
global init
if init:
return
init = True
if output_file:
formatter = logging.Formatter(log_format)
handler = logging.FileHandler(filename=output_file)
handler.setFormatter(formatter)
handler.addFilter(DeopleteLogFilter(vim))
root.addHandler(handler)
level = str(level).upper()
if level not in ('DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR',
'CRITICAL', 'FATAL'):
level = 'DEBUG'
root.setLevel(getattr(logging, level))
try:
import pkg_resources
pynvim_version = pkg_resources.get_distribution('pynvim').version
except Exception:
pynvim_version = 'unknown'
log = getLogger('logging')
log.info('--- Deoplete Log Start ---')
log.info('%s, Python %s, pynvim %s',
vim.call('deoplete#util#neovim_version'),
'.'.join(map(str, sys.version_info[:3])),
pynvim_version)
if 'deoplete#_logging_notified' not in vim.vars:
vim.vars['deoplete#_logging_notified'] = 1
vim.call('deoplete#util#print_debug', 'Logging to %s' % (
output_file))
def logmethod(func: FUNC) -> typing.Callable[[FUNC], FUNC]:
"""Decorator for setting up the logger in LoggingMixin subclasses.
This does not guarantee that log messages will be generated. If
`LoggingMixin.is_debug_enabled` is True, it will be propagated up to the
root 'deoplete' logger.
"""
@wraps(func)
def wrapper(self, # type: ignore
*args: typing.Any,
**kwargs: typing.Any) -> typing.Any:
if not init or not self.is_debug_enabled:
return
if self._logger is None:
self._logger = getLogger(getattr(self, 'name', 'unknown'))
return func(self, *args, **kwargs)
return wrapper
class LoggingMixin(object):
"""Class that adds logging functions to a subclass.
"""
is_debug_enabled = False
_logger = None # type: logging.Logger
@logmethod
def debug(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
self._logger.debug(msg, *args, **kwargs)
@logmethod
def info(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
self._logger.info(msg, *args, **kwargs)
@logmethod
def warning(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
self._logger.warning(msg, *args, **kwargs)
warn = warning
@logmethod
def error(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
self._logger.error(msg, *args, **kwargs)
@logmethod
def exception(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
# This will not produce a log message if there is no exception to log.
self._logger.exception(msg, *args, **kwargs)
@logmethod
def critical(self, msg: str,
*args: typing.Any, **kwargs: typing.Any) -> None:
self._logger.critical(msg, *args, **kwargs)
fatal = critical
class DeopleteLogFilter(logging.Filter):
def __init__(self, vim: Nvim, name: str = ''):
self.vim = vim
self.counter: typing.Dict[str, int] = defaultdict(int)
self.last_message_time: float = 0
self.last_message: typing.Tuple[typing.Any, ...] = ()
def filter(self, record: logging.LogRecord) -> bool:
t = time.time()
elapsed = t - self.last_message_time
self.last_message_time = t
message = (record.levelno, record.name, record.msg, record.args)
if message == self.last_message and elapsed < log_message_cooldown:
# Ignore if the same message comes in too fast.
return False
self.last_message = message
if record.levelno >= logging.ERROR:
self.counter[record.name] += 1
if self.counter[record.name] <= 2:
# Only permit 2 errors in succession from a logging source to
# display errors inside of Neovim. After this, it is no longer
# permitted to emit any more errors and should be addressed.
self.vim.call('deoplete#util#print_error', record.getMessage(),
record.name)
if record.exc_info and record.stack_info:
# Add a penalty for messages that generate exceptions to avoid
# making the log harder to read with doubled stack traces.
self.counter[record.name] += 1
elif self.counter[record.name] < 2:
# If below the threshold for silencing a logging source, reset its
# counter.
self.counter[record.name] = 0
return True

View File

@ -0,0 +1,221 @@
# ============================================================================
# FILE: parent.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import time
import os
import msgpack
import subprocess
import sys
import typing
from abc import abstractmethod
from functools import partial
from pathlib import Path
from queue import Queue
from deoplete import logger
from deoplete.process import Process
from deoplete.util import error_tb, error, Nvim
UserContext = typing.Dict[str, typing.Any]
class _Parent(logger.LoggingMixin):
def __init__(self, vim: Nvim) -> None:
self.name = 'parent'
self._vim = vim
self._loaded_filters: typing.Set[str] = set()
self._start_process()
def enable_logging(self) -> None:
self._put('enable_logging', [])
self.is_debug_enabled = True
def add_source(self, path: str) -> None:
self._put('add_source', [path])
def add_filter(self, path: str) -> None:
if path in self._loaded_filters:
return
self._loaded_filters.add(path)
self._put('add_filter', [path])
def set_source_attributes(self, context: UserContext) -> None:
self._put('set_source_attributes', [context])
def set_custom(self, custom: typing.Any) -> None:
self._put('set_custom', [custom])
def on_event(self, context: UserContext) -> None:
self._put('on_event', [context])
@abstractmethod
def _start_process(self) -> None:
pass
@abstractmethod
def _put(self, name: str,
args: typing.List[typing.Any]) -> typing.Optional[str]:
pass
class SyncParent(_Parent):
def _start_process(self) -> None:
from deoplete.child import Child
self._child = Child(self._vim)
def merge_results(self,
context: UserContext) -> typing.Tuple[typing.Any]:
results = self._child._merge_results(context, queue_id=None)
ret = (results['is_async'], results['is_async'],
results['merged_results']) if results else (False, [])
return ret # type: ignore
def _put(self, name: str,
args: typing.List[typing.Any]) -> typing.Optional[str]:
self._child.main(name, args, queue_id=None)
return None
class AsyncParent(_Parent):
def _get_python_executable(self) -> str:
"""Get Python executable.
This handles Python being embedded in Vim on Windows or OSX.
Taken from jedi.api.environment._try_get_same_env.
"""
exe = sys.executable
if not os.path.basename(exe).lower().startswith('python'):
checks: typing.Tuple[typing.Any, ...]
if sys.platform == 'win32':
checks = (r'Scripts\python.exe', 'python.exe')
else:
checks = (
'bin/python%s.%s' % (
sys.version_info[0], sys.version[1]),
'bin/python%s' % (sys.version_info[0]),
'bin/python',
)
for check in checks:
guess = os.path.join(sys.exec_prefix, check)
if os.path.isfile(str(guess)):
return str(guess)
if 'python3_host_prog' not in self._vim.vars:
return 'python3'
return str(self._vim.vars['python3_host_prog'])
return exe
def _start_process(self) -> None:
self._stdin: typing.Optional[typing.Any] = None
self._queue_id = ''
self._queue_in: 'Queue[bytes]' = Queue()
self._queue_out: 'Queue[typing.Any]' = Queue()
self._queue_err: 'Queue[typing.Any]' = Queue()
if msgpack.version < (1, 0, 0):
self._packer = msgpack.Packer(
encoding='utf-8',
unicode_errors='surrogateescape')
self._unpacker = msgpack.Unpacker(
encoding='utf-8',
unicode_errors='surrogateescape')
else:
self._packer = msgpack.Packer(
unicode_errors='surrogateescape')
self._unpacker = msgpack.Unpacker(
unicode_errors='surrogateescape')
self._prev_pos: typing.List[typing.Any] = []
info = None
if sys.platform == 'win32':
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
main = str(Path(__file__).parent.parent.parent.parent.joinpath(
'autoload', 'deoplete', '_main.py'))
self._hnd = self._vim.loop.create_task(
self._vim.loop.subprocess_exec(
partial(Process, self),
self._get_python_executable(),
main,
self._vim.vars['deoplete#_serveraddr'],
startupinfo=info))
def _print_error(self, message: str) -> None:
error(self._vim, message)
def _connect_stdin(self, stdin: int) -> msgpack.Unpacker:
self._stdin = stdin
return self._unpacker
def merge_results(self,
context: UserContext) -> typing.Tuple[typing.Any, ...]:
# Note: TextChangedP is triggered when Update
event = context['event']
if ((event == 'Update' or event == 'TextChangedP') and
context['position'] == self._prev_pos and self._queue_id):
# Use previous id
queue_id = self._queue_id
else:
queue_id = self._put('merge_results', [context]) # type: ignore
if not queue_id:
return (False, False, [])
get = self._get(queue_id)
if not get:
# Skip the next merge_results
self._queue_id = queue_id
self._prev_pos = context['position']
return (True, False, [])
self._queue_id = ''
results = get[0]
return (results['is_async'], results['is_async'],
results['merged_results']) if results else (False, [])
def _put(self, name: str,
args: typing.List[typing.Any]) -> typing.Optional[str]:
if not self._hnd:
return None
queue_id = str(time.time())
msg = self._packer.pack({
'name': name, 'args': args, 'queue_id': queue_id
})
self._queue_in.put(msg)
if self._stdin:
try:
while not self._queue_in.empty():
self._stdin.write(self._queue_in.get_nowait())
except BrokenPipeError:
error_tb(self._vim, 'Crash in child process')
error(self._vim, 'stderr=' +
str(self._proc.read_error())) # type: ignore
self._hnd = None
return queue_id
def _get(self, queue_id: str) -> typing.List[typing.Any]:
if not self._hnd:
return []
check_stderr = self._vim.call(
'deoplete#custom#_get_option', 'check_stderr')
while check_stderr and not self._queue_err.empty():
self._print_error(self._queue_err.get_nowait())
outs = []
while not self._queue_out.empty():
outs.append(self._queue_out.get_nowait())
try:
return [x for x in outs if x['queue_id'] == queue_id]
except TypeError:
error_tb(self._vim,
'"stdout" seems contaminated by sources. '
'"stdout" is used for RPC; Please pipe or discard')
return []

View File

@ -0,0 +1,33 @@
# ============================================================================
# FILE: process.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import asyncio
import typing
class Process(asyncio.SubprocessProtocol):
def __init__(self, plugin: typing.Any) -> None:
self._plugin = plugin
self._vim = plugin._vim
def connection_made(self, transport: typing.Any) -> None:
self._unpacker = self._plugin._connect_stdin(
transport.get_pipe_transport(0))
def pipe_data_received(self, fd: int, data: typing.Any) -> None:
if fd == 2:
# stderr
self._plugin._queue_err.put(f'stderr from child process:{data}')
return
unpacker = self._unpacker
unpacker.feed(data)
for child_out in unpacker:
self._plugin._queue_out.put(child_out)
def process_exited(self) -> None:
self._plugin._queue_err.put('The child process is exited!')

View File

@ -0,0 +1,74 @@
# ============================================================================
# FILE: around.py
# AUTHOR: Khalidov Oleg <brooth at gmail.com>
# License: MIT license
# ============================================================================
import re
from deoplete.base.source import Base
from deoplete.util import parse_buffer_pattern, getlines
from deoplete.util import Nvim, UserContext, Candidates
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'around'
self.rank = 300
self.vars = {
'mark_above': '[A]',
'mark_below': '[A]',
'mark_changes': '[A]',
'range_above': 20,
'range_below': 20,
}
custom_vars = self.vim.call(
'deoplete#custom#_get_source_vars', self.name
)
if custom_vars:
self.vars.update(custom_vars)
def gather_candidates(self, context: UserContext) -> Candidates:
line = context['position'][1]
candidates: Candidates = []
# lines above
words = parse_buffer_pattern(
reversed(
getlines(
self.vim, max([1, line - self.vars['range_above']]), line
)
),
context['keyword_pattern'],
)
candidates += [
{'word': x, 'menu': self.vars['mark_above']} for x in words
]
# grab ':changes' command output
p = re.compile(r'[\s\d]+')
lines = set()
for change_line in [
x[p.search(x).span()[1]:] # type: ignore
for x in self.vim.call('execute', 'changes').split('\n')[2:]
if p.search(x)
]:
if change_line and change_line != '-invalid-':
lines.add(change_line)
words = parse_buffer_pattern(lines, context['keyword_pattern'])
candidates += [
{'word': x, 'menu': self.vars['mark_changes']} for x in words
]
# lines below
words = parse_buffer_pattern(
getlines(self.vim, line, line + self.vars['range_below']),
context['keyword_pattern'],
)
candidates += [
{'word': x, 'menu': self.vars['mark_below']} for x in words
]
return candidates

View File

@ -0,0 +1,14 @@
# ============================================================================
# FILE: base.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
# For backward compatibility
from deoplete.base.source import Base as _Base
from deoplete.util import Nvim
class Base(_Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)

View File

@ -0,0 +1,70 @@
# ============================================================================
# FILE: buffer.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import typing
from deoplete.base.source import Base
from deoplete.util import parse_buffer_pattern, getlines
from deoplete.util import Nvim, UserContext, Candidates
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'buffer'
self.mark = '[B]'
self.events = ['Init', 'BufReadPost', 'BufWritePost', 'InsertLeave']
self.vars = {
'require_same_filetype': True,
}
self._limit = 1000000
self._buffers: typing.Dict[int, typing.Any] = {}
self._max_lines = 5000
def on_event(self, context: UserContext) -> None:
self._make_cache(context)
tab_bufnrs = self.vim.call('tabpagebuflist')
self._buffers = {
x['bufnr']: x for x in self._buffers.values()
if x['bufnr'] in tab_bufnrs or
self.vim.call('buflisted', x['bufnr'])
}
def gather_candidates(self, context: UserContext) -> Candidates:
tab_bufnrs = self.vim.call('tabpagebuflist')
same_filetype = self.get_var('require_same_filetype')
return {'sorted_candidates': [ # type: ignore
x['candidates'] for x in self._buffers.values()
if not same_filetype or
x['filetype'] in context['filetypes'] or
x['filetype'] in context['same_filetypes'] or
x['bufnr'] in tab_bufnrs
]}
def _make_cache(self, context: UserContext) -> None:
# Bufsize check
size = self.vim.call('line2byte',
self.vim.call('line', '$') + 1) - 1
if size > self._limit:
return
try:
self._buffers[context['bufnr']] = {
'bufnr': context['bufnr'],
'filetype': self.get_buf_option('filetype'),
'candidates': [
{'word': x} for x in
sorted(parse_buffer_pattern(getlines(self.vim),
context['keyword_pattern']),
key=str.lower)
]
}
except UnicodeDecodeError:
return

View File

@ -0,0 +1,100 @@
# ============================================================================
# FILE: file.py
# AUTHOR: Felipe Morales <hel.sheep at gmail.com>
# Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import os
import re
import typing
from os.path import exists, dirname
from deoplete.base.source import Base
from deoplete.util import expand, Nvim, UserContext, Candidates
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'file'
self.mark = '[F]'
self.min_pattern_length = 0
self.rank = 150
self.events: typing.List[str] = ['InsertEnter']
self.vars = {
'enable_buffer_path': True,
'force_completion_length': -1,
}
self._isfname = ''
def on_event(self, context: UserContext) -> None:
self._isfname = self.vim.call(
'deoplete#util#vimoption2python_not',
self.vim.options['isfname'])
def get_complete_position(self, context: UserContext) -> int:
pos = int(context['input'].rfind('/'))
force_completion_length = int(
self.get_var('force_completion_length')) # type: ignore
if pos < 0 and force_completion_length >= 0:
fmt = '[a-zA-Z0-9.-]{{{}}}$'.format(force_completion_length)
m = re.search(fmt, context['input'])
if m:
return m.start()
return pos if pos < 0 else pos + 1
def gather_candidates(self, context: UserContext) -> Candidates:
if not self._isfname:
self.on_event(context)
input_str = (context['input']
if context['input'].rfind('/') >= 0
else './')
p = self._longest_path_that_exists(context, input_str)
if not p or p == '/' or re.search('//+$', p):
return []
complete_str = self._substitute_path(context, dirname(p) + '/')
if not os.path.isdir(complete_str):
return []
hidden = context['complete_str'].find('.') == 0
contents: typing.List[typing.Any] = [[], []]
try:
for item in sorted(os.listdir(complete_str), key=str.lower):
if not hidden and item[0] == '.':
continue
contents[not os.path.isdir(complete_str + item)].append(item)
except PermissionError:
pass
dirs, files = contents
return [{'word': x, 'abbr': x + '/'} for x in dirs
] + [{'word': x} for x in files]
def _longest_path_that_exists(self, context: UserContext,
input_str: str) -> str:
input_str = re.sub(r'[^/]*$', '', input_str)
data = re.split(r'((?:%s+|(?:(?<![\w\s/\.])(?:~|\.{1,2})?/)+))' %
self._isfname, input_str)
data = [''.join(data[i:]) for i in range(len(data))]
existing_paths = sorted(filter(lambda x: exists(
dirname(self._substitute_path(context, x))), data))
return existing_paths[-1] if existing_paths else ''
def _substitute_path(self, context: UserContext, path: str) -> str:
m = re.match(r'(\.{1,2})/+', path)
if m:
if self.get_var('enable_buffer_path') and context['bufpath']:
base = context['bufpath']
else:
base = os.path.join(context['cwd'], 'x')
for _ in m.group(1):
base = dirname(base)
return os.path.abspath(os.path.join(
base, path[len(m.group(0)):])) + '/'
return expand(path)

View File

@ -0,0 +1,66 @@
# ============================================================================
# FILE: member.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
import typing
from deoplete.base.source import Base
from deoplete.util import (
convert2list, parse_buffer_pattern, set_pattern, getlines)
from deoplete.util import Nvim, UserContext, Candidates
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'member'
self.mark = '[M]'
self.min_pattern_length = 0
self._object_pattern = r'[a-zA-Z_]\w*(?:\(\)?)?'
self._prefix = ''
prefix_patterns: typing.Dict[str, str] = {}
set_pattern(prefix_patterns,
'_', r'\.')
set_pattern(prefix_patterns,
'c,objc', [r'\.', '->'])
set_pattern(prefix_patterns,
'cpp,objcpp', [r'\.', '->', '::'])
set_pattern(prefix_patterns,
'perl,php', ['->'])
set_pattern(prefix_patterns,
'ruby', [r'\.', '::'])
set_pattern(prefix_patterns,
'lua', [r'\.', ':'])
self.vars = {
'prefix_patterns': prefix_patterns,
}
def get_complete_position(self, context: UserContext) -> int:
# Check member prefix pattern.
for prefix_pattern in convert2list(
self.get_filetype_var(
context['filetype'], 'prefix_patterns')):
m = re.search(self._object_pattern + prefix_pattern + r'\w*$',
context['input'])
if m is None or prefix_pattern == '':
continue
self._prefix = re.sub(r'\w*$', '', m.group(0))
m = re.search(r'\w*$', context['input'])
if m:
return m.start()
return -1
def gather_candidates(self, context: UserContext) -> Candidates:
return [{'word': x} for x in
parse_buffer_pattern(
getlines(self.vim),
r'(?<=' + re.escape(self._prefix) + r')\w+'
)
if x != context['complete_str']]

View File

@ -0,0 +1,98 @@
# ============================================================================
# FILE: omni.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
import typing
from deoplete.base.source import Base
from deoplete.util import (
convert2list, set_pattern, convert2candidates)
from deoplete.util import Nvim, UserContext, Candidates
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'omni'
self.mark = '[O]'
self.rank = 500
self.is_bytepos = True
self.min_pattern_length = 0
input_patterns: typing.Dict[str, str] = {}
set_pattern(input_patterns, 'css,less,scss,sass',
[r'\w{2}', r'\w+:?\s*\w*', r'[@!]'])
self.vars = {
'input_patterns': input_patterns,
'functions': {},
}
def get_complete_position(self, context: UserContext) -> int:
current_ft = self.get_buf_option('filetype')
for filetype in list(set([context['filetype']] +
context['filetype'].split('.'))):
pos = self._get_complete_position(context, current_ft, filetype)
if pos >= 0:
return pos
return -1
def _get_complete_position(self, context: UserContext,
current_ft: str, filetype: str) -> int:
for omnifunc in convert2list(
self.get_filetype_var(filetype, 'functions')):
if omnifunc == '' and (filetype == current_ft or
filetype in ['css', 'javascript']):
omnifunc = self.get_buf_option('omnifunc')
if omnifunc == '':
continue
self._omnifunc = omnifunc
for input_pattern in convert2list(
self.get_filetype_var(filetype, 'input_patterns')):
m = re.search('(' + input_pattern + ')$', context['input'])
# self.debug(filetype)
# self.debug(input_pattern)
if input_pattern == '' or (context['event'] !=
'Manual' and m is None):
continue
if self._omnifunc in [
'ccomplete#Complete',
'htmlcomplete#CompleteTags',
'LanguageClient#complete',
'rubycomplete#Complete',
'phpcomplete#CompletePHP']:
# In the blacklist
return -1
try:
complete_pos = int(self.vim.call(self._omnifunc, 1, ''))
except Exception:
self.print_error('Error occurred calling omnifunction: ' +
self._omnifunc)
return -1
return complete_pos
return -1
def gather_candidates(self, context: UserContext) -> Candidates:
try:
candidates = self.vim.call(self._omnifunc, 0, '')
if isinstance(candidates, dict):
candidates = candidates['words']
elif not isinstance(candidates, list):
candidates = []
except Exception:
candidates = []
candidates = convert2candidates(candidates)
for candidate in candidates:
candidate['dup'] = 1
candidate['equal'] = 1
return candidates # type: ignore

View File

@ -0,0 +1,298 @@
# ============================================================================
# FILE: util.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import os
import re
import sys
import glob
import importlib.util
import traceback
import typing
import unicodedata
if importlib.util.find_spec('pynvim'):
from pynvim import Nvim
from pynvim.api import Buffer
else:
from neovim import Nvim
from neovim.api import Buffer
UserContext = typing.Dict[str, typing.Any]
Candidate = typing.Dict[str, typing.Any]
Candidates = typing.List[Candidate]
def set_pattern(variable: typing.Dict[str, str],
keys: str, pattern: typing.Any) -> None:
for key in keys.split(','):
variable[key] = pattern
def convert2list(expr: typing.Any) -> typing.List[typing.Any]:
return (expr if isinstance(expr, list) else [expr])
def convert2candidates(li: typing.Any) -> Candidates:
ret = []
if li and isinstance(li, list):
for x in li:
if isinstance(x, str):
ret.append({'word': x})
else:
ret.append(x)
else:
ret = li
return ret
def globruntime(runtimepath: str, path: str) -> typing.List[str]:
ret: typing.List[str] = []
for rtp in runtimepath.split(','):
ret += glob.glob(rtp + '/' + path)
return ret
def import_plugin(path: str, source: str,
classname: str) -> typing.Optional[typing.Any]:
"""Import Deoplete plugin source class.
If the class exists, add its directory to sys.path.
"""
name = os.path.splitext(os.path.basename(path))[0]
module_name = 'deoplete.%s.%s' % (source, name)
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
cls = getattr(module, classname, None)
if not cls:
return None
dirname = os.path.dirname(path)
if dirname not in sys.path:
sys.path.insert(0, dirname)
return cls
def debug(vim: Nvim, expr: typing.Any) -> None:
if hasattr(vim, 'out_write'):
vim.out_write(f'[deoplete] {expr}\n')
else:
vim.call('deoplete#util#print_debug', expr)
def error(vim: Nvim, expr: typing.Any) -> None:
if hasattr(vim, 'err_write'):
vim.err_write(f'[deoplete] {expr}\n')
else:
vim.call('deoplete#util#print_error', expr)
def error_tb(vim: Nvim, msg: str) -> None:
lines: typing.List[str] = []
t, v, tb = sys.exc_info()
if t and v and tb:
lines += traceback.format_exc().splitlines()
lines += ['%s. Use :messages / see above for error details.' % msg]
if hasattr(vim, 'err_write'):
vim.err_write('[deoplete] %s\n' % '\n'.join(lines))
else:
for line in lines:
vim.call('deoplete#util#print_error', line)
def error_vim(vim: Nvim, msg: str) -> None:
throwpoint = vim.eval('v:throwpoint')
if throwpoint != '':
error(vim, 'v:throwpoint = ' + throwpoint)
exception = vim.eval('v:exception')
if exception != '':
error(vim, 'v:exception = ' + exception)
error_tb(vim, msg)
def escape(expr: str) -> str:
return expr.replace("'", "''")
def charpos2bytepos(encoding: str, text: str, pos: int) -> int:
return len(bytes(text[: pos], encoding, errors='replace'))
def bytepos2charpos(encoding: str, text: str, pos: int) -> int:
return len(bytes(text, encoding, errors='replace')[: pos].decode(
encoding, errors='replace'))
def get_custom(custom: typing.Dict[str, typing.Any],
source_name: str, key: str,
default: typing.Any) -> typing.Any:
custom_source = custom['source']
if source_name not in custom_source:
return get_custom(custom, '_', key, default)
elif key in custom_source[source_name]:
return custom_source[source_name][key]
elif key in custom_source['_']:
return custom_source['_'][key]
else:
return default
def get_syn_names(vim: Nvim) -> str:
return str(vim.call('deoplete#util#get_syn_names'))
def parse_file_pattern(f: typing.Iterable[str],
pattern: str) -> typing.List[str]:
p = re.compile(pattern)
ret: typing.List[str] = []
for li in f:
ret += p.findall(li)
return list(set(ret))
def parse_buffer_pattern(b: Buffer, pattern: str) -> typing.List[str]:
return list(set(re.compile(pattern).findall('\n'.join(b))))
def fuzzy_escape(string: str, camelcase: bool) -> str:
# Escape string for python regexp.
p = re.sub(r'([a-zA-Z0-9_])', r'\1.*', re.escape(string))
if camelcase and re.search(r'[A-Z]', string):
p = re.sub(r'([a-z])', (lambda pat:
f'[{pat.group(1)}{pat.group(1).upper()}]'), p)
p = re.sub(r'([a-zA-Z0-9_])\.\*', r'\1[^\1]*', p)
return p
def load_external_module(base: str, module: str) -> None:
current = os.path.dirname(os.path.abspath(base))
module_dir = os.path.join(os.path.dirname(current), module)
if module_dir not in sys.path:
sys.path.insert(0, module_dir)
def truncate_skipping(string: str, max_width: int,
footer: str, footer_len: int) -> str:
if not string:
return ''
if len(string) <= max_width / 2:
return string
if strwidth(string) <= max_width:
return string
footer += string[
-len(truncate(string[::-1], footer_len)):]
return truncate(string, max_width - strwidth(footer)) + footer
def truncate(string: str, max_width: int) -> str:
if len(string) <= max_width / 2:
return string
if strwidth(string) <= max_width:
return string
width = 0
ret = ''
for c in string:
wc = charwidth(c)
if width + wc > max_width:
break
ret += c
width += wc
return ret
def strwidth(string: str) -> int:
width = 0
for c in string:
width += charwidth(c)
return width
def charwidth(c: str) -> int:
wc = unicodedata.east_asian_width(c)
return 2 if wc == 'F' or wc == 'W' else 1
def expand(path: str) -> str:
return os.path.expanduser(os.path.expandvars(path))
def getlines(vim: Nvim, start: int = 1,
end: typing.Union[int, str] = '$') -> typing.List[str]:
if end == '$':
end = len(vim.current.buffer)
max_len = min([int(end) - start, 5000])
lines: typing.List[str] = []
current = start
while current <= int(end):
lines += vim.call('getline', current, current + max_len)
current += max_len + 1
return lines
def binary_search_begin(li: typing.List[Candidates], prefix: str) -> int:
if not li:
return -1
if len(li) == 1:
word = li[0]['word'] # type: ignore
return 0 if word.lower().startswith(prefix) else -1
s = 0
e = len(li)
prefix = prefix.lower()
while s < e:
index = int((s + e) / 2)
word = li[index]['word'].lower() # type: ignore
if word.startswith(prefix):
if (index - 1) < 0:
return index
prev_word = li[index-1]['word'] # type: ignore
if not prev_word.lower().startswith(prefix):
return index
e = index
elif prefix < word:
e = index
else:
s = index + 1
return -1
def binary_search_end(li: typing.List[Candidates], prefix: str) -> int:
if not li:
return -1
if len(li) == 1:
word = li[0]['word'] # type: ignore
return 0 if word.lower().startswith(prefix) else -1
s = 0
e = len(li)
prefix = prefix.lower()
while s < e:
index = int((s + e) / 2)
word = li[index]['word'].lower() # type: ignore
if word.startswith(prefix):
if (index + 1) >= len(li):
return index
next_word = li[index+1]['word'] # type: ignore
if not next_word.lower().startswith(prefix):
return index
s = index + 1
elif prefix < word:
e = index
else:
s = index + 1
return -1
def uniq_list_dict(li: typing.List[typing.Any]) -> typing.List[typing.Any]:
# Uniq list of dictionaries
ret: typing.List[typing.Any] = []
for d in li:
if d not in ret:
ret.append(d)
return ret

View File

@ -0,0 +1,123 @@
let s:suite = themis#suite('custom')
let s:assert = themis#helper('assert')
function! s:suite.custom_source() abort
call deoplete#custom#_init()
call deoplete#custom#source('_',
\ 'matchers', ['matcher_head'])
call deoplete#custom#source('_', 'converters',
\ ['converter_auto_delimiter', 'remove_overlap'])
call s:assert.equals(
\ deoplete#custom#_get().source,
\ {'_' : {
\ 'matchers': ['matcher_head'],
\ 'converters': ['converter_auto_delimiter', 'remove_overlap']}})
call deoplete#custom#_init()
call deoplete#custom#source('buffer',
\ 'min_pattern_length', 9999)
call deoplete#custom#source('buffer', 'rank', 9999)
call deoplete#custom#source('buffer', {'filetypes': []})
call s:assert.equals(
\ deoplete#custom#_get_source('buffer').filetypes, [])
call s:assert.equals(
\ deoplete#custom#_get_source('buffer').rank, 9999)
call deoplete#custom#var('file', 'force_completion_length', 2)
call deoplete#custom#var('file', {'foo': -1, 'bar': 1})
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_source_vars('file'),
\ {'force_completion_length' : 2, 'foo': -1, 'bar': 1})
call deoplete#custom#buffer_var('file', 'force_completion_length', 0)
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_source_vars('file'),
\ {'force_completion_length' : 0, 'foo': -1, 'bar': 1})
endfunction
function! s:suite.custom_option() abort
" Simple option test
call deoplete#custom#_init()
call deoplete#custom#_init_buffer()
call deoplete#custom#option('auto_complete', v:true)
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_option('auto_complete'), v:true)
" Buffer option test
call deoplete#custom#buffer_option('auto_complete', v:false)
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_option('auto_complete'), v:false)
" Compatibility test
call deoplete#custom#_init()
call deoplete#custom#_init_buffer()
let g:deoplete#disable_auto_complete = 1
call deoplete#init#_custom_variables()
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_option('auto_complete'), v:false)
" Filetype option test
call deoplete#custom#_init()
call deoplete#custom#_init_buffer()
let s:java_pattern = '[^. *\t]\.\w*'
call deoplete#custom#option('omni_patterns', {
\ 'java': s:java_pattern,
\})
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_filetype_option(
\ 'omni_patterns', 'java', ''), s:java_pattern)
call s:assert.equals(
\ deoplete#custom#_get_filetype_option(
\ 'omni_patterns', 'foobar', ''), '')
" Compatibility test
call deoplete#custom#_init()
call deoplete#custom#_init_buffer()
let s:tex_pattern = '[^\w|\s][a-zA-Z_]\w*'
let g:deoplete#keyword_patterns = {}
let g:deoplete#keyword_patterns.tex = '[^\w|\s][a-zA-Z_]\w*'
call deoplete#init#_custom_variables()
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_filetype_option(
\ 'keyword_patterns', 'tex', ''), s:tex_pattern)
" Dict type format
call deoplete#custom#_init()
call deoplete#custom#_init_buffer()
call deoplete#custom#option({
\ 'auto_complete': v:true, 'camel_case': v:true
\ })
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_option('auto_complete'), v:true)
call s:assert.equals(
\ deoplete#custom#_get_option('camel_case'), v:true)
endfunction
function! s:suite.custom_filter() abort
call deoplete#custom#_init()
call deoplete#custom#filter('converter_auto_delimiter', {
\ 'delimiters': ['foo', 'bar'],
\ })
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_filter('converter_auto_delimiter'),
\ {'delimiters': ['foo', 'bar']})
call deoplete#custom#buffer_filter('converter_auto_delimiter', {
\ 'delimiters': ['foo'],
\ })
call deoplete#custom#_update_cache()
call s:assert.equals(
\ deoplete#custom#_get_filter('converter_auto_delimiter'),
\ {'delimiters': ['foo']})
endfunction

View File

@ -0,0 +1,35 @@
let s:suite = themis#suite('parser')
let s:assert = themis#helper('assert')
function! s:suite.vimoption2python() abort
call s:assert.equals(
\ deoplete#util#vimoption2python('@,48-57,_,\'), '[\w@0-9_\\]')
call s:assert.equals(
\ deoplete#util#vimoption2python('@,-,48-57,_'), '[\w@0-9_-]')
call s:assert.equals(
\ deoplete#util#vimoption2python('@,,,48-57,_'), '[\w@,0-9_]')
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.10', '0.1.8'), 2)
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.10', '0.1.10'), 0)
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.10', '0.1.0010'), 0)
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.1', '0.1.8'), -7)
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.1000', '0.1.10'), 990)
call s:assert.equals(
\ deoplete#util#versioncmp('0.1.0001', '0.1.10'), -9)
call s:assert.equals(
\ deoplete#util#versioncmp('2.0.1', '1.3.5'), 9696)
call s:assert.equals(
\ deoplete#util#versioncmp('3.2.1', '0.0.0'), 30201)
call s:assert.equals(
\ deoplete#util#vimoption2python('45,48-57,65-90,95,97-122'),
\ '[\w0-9A-Z_a-z-]')
call s:assert.equals(
\ deoplete#util#vimoption2python('33,35-39,42-43,45-58,60-90,94,95,97-122,126'),
\ '[\w!#-''*-+\--:<-Z^_a-z~]')
call s:assert.equals(
\ deoplete#util#vimoption2python('33-45'), '[\w!-\-]')
endfunction

View File

@ -0,0 +1,5 @@
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, os.path.join(BASE_DIR, 'rplugin/python3'))

View File

@ -0,0 +1,6 @@
pynvim
pytest
flake8
mypy
vim-vint
pytest-cov

View File

@ -0,0 +1,41 @@
from deoplete.filter.converter_reorder_attr import Filter
candidates = [
{'word': 'Apple', 'kind': 'Fruit'},
{'word': 'Banana', 'kind': 'Fruit'},
{'word': 'Pen', 'kind': 'Object'},
{'word': 'Cherry Pie', 'kind': 'Pie'},
]
def test_reorder():
candidates_copy = candidates[:]
preferred_order = {'kind': ['Pie', 'Fruit']}
expected_candidates = [
{'word': 'Cherry Pie', 'kind': 'Pie'},
{'word': 'Apple', 'kind': 'Fruit'},
{'word': 'Banana', 'kind': 'Fruit'},
{'word': 'Pen', 'kind': 'Object'},
]
assert expected_candidates == Filter.filter_attrs(
candidates_copy, preferred_order
)
def test_filter():
candidates_copy = candidates[:]
preferred_order = {'word': ['!Pen', 'Banana']}
expected_candidates = [
{'word': 'Banana', 'kind': 'Fruit'},
{'word': 'Apple', 'kind': 'Fruit'},
{'word': 'Cherry Pie', 'kind': 'Pie'},
]
assert expected_candidates == Filter.filter_attrs(
candidates_copy, preferred_order
)

View File

@ -0,0 +1,41 @@
import deoplete.util as util
from deoplete.filter.converter_remove_overlap import overlap_length
def test_fuzzy_escapse():
assert util.fuzzy_escape('foo', 0) == 'f[^f]*o[^o]*o[^o]*'
assert util.fuzzy_escape('foo', 1) == 'f[^f]*o[^o]*o[^o]*'
assert util.fuzzy_escape('Foo', 1) == 'F[^F]*[oO].*[oO].*'
def test_overlap_length():
assert overlap_length('foo bar', 'bar baz') == 3
assert overlap_length('foobar', 'barbaz') == 3
assert overlap_length('foob', 'baz') == 1
assert overlap_length('foobar', 'foobar') == 6
assert overlap_length('тест', 'ст') == len('ст')
def test_charwidth():
assert util.charwidth('f') == 1
assert util.charwidth('') == 2
def test_strwidth():
assert util.strwidth('foo bar') == 7
assert util.strwidth('あいうえ') == 8
assert util.strwidth('fooあい') == 7
def test_truncate():
assert util.truncate('foo bar', 3) == 'foo'
assert util.truncate('fooあい', 5) == 'fooあ'
assert util.truncate('あいうえ', 4) == 'あい'
assert util.truncate('fooあい', 4) == 'foo'
def test_skipping():
assert util.truncate_skipping('foo bar', 3, '..', 3) == '..bar'
assert util.truncate_skipping('foo bar', 6, '..', 3) == 'f..bar'
assert util.truncate_skipping('fooあい', 5, '..', 3) == 'f..い'
assert util.truncate_skipping('あいうえ', 6, '..', 2) == 'あ..え'

View File

@ -0,0 +1,59 @@
from deoplete.filter.matcher_full_fuzzy import Filter
from test_matcher_fuzzy import _ctx
def test_matcher_full_fuzzy():
f = Filter(None)
assert f.name == 'matcher_full_fuzzy'
assert f.description == 'full fuzzy matcher'
ctx = _ctx('')
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'Foobar' },
{ 'word': 'aFoobar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
ctx = _ctx('FOBR')
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'Foobar' },
{ 'word': 'aFoobar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
ctx = _ctx('foBr', ignorecase=False)
assert f.filter(ctx) == [
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
ctx = _ctx('fobr', camelcase=False)
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'Foobar' },
{ 'word': 'aFoobar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
ctx = _ctx('fobr', ignorecase=False, camelcase=False)
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
]

View File

@ -0,0 +1,68 @@
from deoplete.filter.matcher_fuzzy import Filter
def _ctx(complete_str, ignorecase=True, camelcase=True):
_candidates = [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'Foobar' },
{ 'word': 'aFoobar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
return {
'complete_str' : complete_str,
'ignorecase' : ignorecase,
'camelcase' : camelcase,
'is_sorted' : False,
'candidates' : _candidates
}
def test_matcher_fuzzy():
f = Filter(None)
assert f.name == 'matcher_fuzzy'
assert f.description == 'fuzzy matcher'
ctx = _ctx('')
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'afoobar' },
{ 'word': 'fooBar' },
{ 'word': 'afooBar' },
{ 'word': 'Foobar' },
{ 'word': 'aFoobar' },
{ 'word': 'FooBar' },
{ 'word': 'aFooBar' },
]
ctx = _ctx('FOBR')
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'fooBar' },
{ 'word': 'Foobar' },
{ 'word': 'FooBar' },
]
ctx = _ctx('foBr', ignorecase=False)
assert f.filter(ctx) == [
{ 'word': 'fooBar' },
{ 'word': 'FooBar' },
]
ctx = _ctx('fobr', camelcase=False)
assert f.filter(ctx) == [
{ 'word': 'foobar' },
{ 'word': 'fooBar' },
{ 'word': 'Foobar' },
{ 'word': 'FooBar' },
]
ctx = _ctx('fobr', ignorecase=False, camelcase=False)
assert f.filter(ctx) == [
{ 'word': 'foobar' },
]

View File

@ -0,0 +1,69 @@
import deoplete.util as util
def test_pos():
assert util.bytepos2charpos('utf-8', 'foo bar', 3) == 3
assert util.bytepos2charpos('utf-8', 'あああ', 3) == 1
assert util.charpos2bytepos('utf-8', 'foo bar', 3) == 3
assert util.charpos2bytepos('utf-8', 'あああ', 3) == 9
def test_custom():
custom = {'source':{}}
custom['source'] = {'_': {'mark': ''}, 'java': {'converters': []}}
assert util.get_custom(custom, 'java', 'mark', 'foobar') == ''
assert util.get_custom(custom, 'java', 'converters', 'foobar') == []
assert util.get_custom(custom, 'foo', 'mark', 'foobar') == ''
assert util.get_custom(custom, 'foo', 'converters', 'foobar') == 'foobar'
def test_globruntime():
assert util.globruntime('/usr', 'bin') == ['/usr/bin']
def test_binary_search():
assert util.binary_search_begin([], '') == -1
assert util.binary_search_begin([{'word': 'abc'}], 'abc') == 0
assert util.binary_search_begin([
{'word': 'aaa'}, {'word': 'abc'},
], 'abc') == 1
assert util.binary_search_begin([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'abc'},
], 'abc') == 2
assert util.binary_search_begin([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'AbC'},
], 'abc') == 2
assert util.binary_search_begin([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'abc'},
], 'b') == -1
assert util.binary_search_begin([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'aac'}, {'word': 'abc'},
], 'aa') == 1
assert util.binary_search_end([], '') == -1
assert util.binary_search_end([{'word': 'abc'}], 'abc') == 0
assert util.binary_search_end([
{'word': 'aaa'}, {'word': 'abc'},
], 'abc') == 1
assert util.binary_search_end([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'abc'},
], 'abc') == 2
assert util.binary_search_end([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'abc'},
], 'b') == -1
assert util.binary_search_end([
{'word': 'a'}, {'word': 'aaa'}, {'word': 'aac'}, {'word': 'abc'},
], 'aa') == 2
def test_uniq_list_dict():
assert util.uniq_list_dict([
{'abbr': 'word', 'word': 'foobar'},
{'word': 'bar'},
{'word': 'foobar', 'abbr': 'word'},
{'word': 'baz'},
]) == [
{'word': 'foobar', 'abbr': 'word'},
{'word': 'bar'},
{'word': 'baz'}
]

5
bundle/nvim-yarp/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
__pycache__
# demo
/plugin/hello.vim
/pythonx/hello.py

20
bundle/nvim-yarp/LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright © 2018 roxma@qq.com
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.

120
bundle/nvim-yarp/README.md Normal file
View File

@ -0,0 +1,120 @@
# Yet Another Remote Plugin Framework for Neovim
This is my attempt on writing a remote plugin framework without
`:UpdateRemotePlugins`.
## Requirements
- `has('python3')`
- For Vim 8:
- [roxma/vim-hug-neovim-rpc](https://github.com/roxma/vim-hug-neovim-rpc)
- `g:python3_host_prog` pointed to your python3 executable, or `echo
exepath('python3')` is not empty.
- [pynvim](https://github.com/neovim/pynvim) (`pip3
install pynvim`)
## Use case
- [shougo/deoplete.nvim](https://github.com/shougo/deoplete.nvim)
- [ncm2/ncm2](https://github.com/ncm2/ncm2) and most of its plugins
## Usage
pythonx/hello.py
```python
import vim, time
def greet():
time.sleep(3)
vim.command('echo "Hello world"')
```
plugin/hello.vim
```vim
" Create a python3 process running the hello module. The process is lazy load.
let s:hello = yarp#py3('hello')
com HelloSync call s:hello.request('greet')
com HelloAsync call s:hello.notify('greet')
" You could type :Hello greet
com -nargs=1 Hello call s:hello.request(<f-args>)
```
## Debugging
Add logging settigns to your vimrc. Log files will be generated with prefix
`/tmp/nvim_log`. An alternative is to export environment variables before
starting vim/nvim.
```vim
let $NVIM_PYTHON_LOG_FILE="/tmp/nvim_log"
let $NVIM_PYTHON_LOG_LEVEL="DEBUG"
```
## Example for existing neovim rplugin porting to Vim 8
More realistic examples could be found at
[nvim-typescript#84](https://github.com/mhartington/nvim-typescript/pull/84),
[deoplete#553](https://github.com/Shougo/deoplete.nvim/pull/553),
[callmekohei/quickdebug](https://github.com/callmekohei/quickdebug).
Now let's consider the following simple rplugin.
After `UpdateRemotePlugins` and restarting neovim, you get `foobar` by `:echo
Bar()`.
```python
# rplugin/python3/foo.py
import pynvim
@pynvim.plugin
class Foo(object):
def __init__(self, vim):
self._vim = vim
@pynvim.function("Bar", sync=True)
def bar(self, args):
return 'hello' + str(args)
```
For working on Vim 8, you need to add these two files:
```vim
" plugin/foo.vim
if has('nvim')
finish
endif
let s:foo = yarp#py3('foo_wrap')
func! Bar(v)
return s:foo.call('bar',a:v)
endfunc
```
```python
# pythonx/foo_wrap.py
from foo import Foo as _Foo
import vim
_obj = _Foo(vim)
def bar(*args):
return _obj.bar(args)
```
How to use
```
$ vim
: echo bar('world')
hello('world',)
```

View File

@ -0,0 +1,25 @@
func! yarp#py3(module)
if type(a:module) == v:t_string
let rp = {}
let rp.module = a:module
else
let rp = a:module
endif
let rp.init = function('yarp#pyx#init')
let rp.type = 'py3'
return yarp#core#new(rp)
endfunc
func! yarp#py(module)
if type(a:module) == v:t_string
let rp = {}
let rp.module = a:module
else
let rp = a:module
endif
let rp.init = function('yarp#pyx#init')
let rp.type = 'py'
return yarp#core#new(rp)
endfunc

View File

@ -0,0 +1,188 @@
if get(s:, 'loaded', 0)
finish
endif
let s:loaded = 1
let s:id = 1
let s:reg = {}
let s:leaving = 0
augroup yarp
autocmd!
" this one is which you're most likely to use?
autocmd VimLeavePre * let s:leaving = 1
augroup end
if has('nvim')
let s:rpcrequest = 'rpcrequest'
let s:rpcnotify = 'rpcnotify'
let s:jobstart = 'jobstart'
fun! s:_serveraddr()
return v:servername
endfunc
let s:serveraddr = function('s:_serveraddr')
else
let s:rpcrequest = get(g:, 'yarp_rpcrequest', 'neovim_rpc#rpcrequest')
let s:rpcnotify = get(g:, 'yarp_rpcnotify', 'neovim_rpc#rpcnotify')
let s:jobstart = get(g:, 'yarp_jobstart', 'neovim_rpc#jobstart')
let s:serveraddr = get(g:, 'yarp_serveraddr', 'neovim_rpc#serveraddr')
endif
func! yarp#core#new(rp)
let s:id = s:id + 1
let rp = a:rp
let rp.jobstart = function('yarp#core#jobstart')
func rp.error(msg) dict
call yarp#core#error(self.module, a:msg)
endfunc
let rp.call = function('yarp#core#request')
let rp.request = function('yarp#core#request')
let rp.notify = function('yarp#core#notify')
let rp.try_notify = function('yarp#core#try_notify')
let rp.wait_channel = function('yarp#core#wait_channel')
let rp.id = s:id
let rp.job_is_dead = 0
let s:reg[rp.id] = rp
" options
let rp.on_load = get(rp, 'on_load', function('yarp#core#_nop'))
let rp.job_detach = get(rp, 'job_detach', 0)
" reserved for user
let rp.user_data = get(rp, 'user_data', {})
return rp
endfunc
func! yarp#core#_nop(...) dict
endfunc
func! yarp#core#on_stderr(chan_id, data, event) dict
let mod = self.self
call mod.error(filter(a:data, 'len(v:val)'))
endfunc
func! yarp#core#on_exit(chan_id, data, event) dict
let mod = self.self
let mod.job_is_dead = 1
if has_key(mod, 'channel')
unlet mod.channel
endif
if has("nvim")
if v:exiting is 0
return
endif
elseif v:dying || s:leaving
return
endif
call mod.error("Job is dead. cmd=" . string(mod.cmd))
endfunc
func! yarp#core#channel_started(id, channel)
let rp = s:reg[a:id]
let rp.channel = a:channel
call call(rp.on_load, [], rp)
endfunc
func! yarp#core#request(method, ...) dict
call self.wait_channel()
return call(s:rpcrequest, [self.channel, a:method] + a:000)
endfunc
func! yarp#core#notify(method, ...) dict
call self.wait_channel()
call call(s:rpcnotify, [self.channel, a:method] + a:000)
endfunc
func! yarp#core#try_notify(method, ...) dict
call self.jobstart()
if get(self, 'job_is_dead', 0)
call self.error('try_notify ' . a:method . ' failed, job is dead')
return 0
endif
if !has_key(self, 'channel')
" not yet started
return 0
endif
let args = [self.channel, a:method] + a:000
try
call call(s:rpcnotify, args)
return 1
catch
call self.error('try_notify ' . s:rpcnotify . ' ' . a:method . ' failed: ' . v:exception . ', ' . string(args))
return 0
endtry
endfunc
func! yarp#core#wait_channel() dict
if has_key(self, 'channel')
return
endif
if ! has_key(self, 'job')
call self.jobstart()
endif
if get(self, 'job', -1) == -1
throw '[yarp] [' . self.module . '] job is not running'
endif
let cnt = 5000 / 20
while ! has_key(self, 'channel')
if self.job_is_dead
throw '[yarp] [' . self.module .
\ '] job is dead. failed establishing channel for ' .
\ string(self.cmd)
endif
if cnt <= 0
throw '[yarp] [' . self.module . '] timeout establishing channel for ' . string(self.cmd)
endif
let cnt = cnt - 1
silent sleep 20m
endwhile
endfunc
func! yarp#core#jobstart() dict
if ! has_key(self, 'cmd')
call self.init()
if ! has_key(self, 'cmd')
call self.error("cmd of the job is not set")
return
endif
endif
if has_key(self, 'job')
return
endif
let opts = {'on_stderr': function('yarp#core#on_stderr'),
\ 'on_exit': function('yarp#core#on_exit'),
\ 'detach': self.job_detach,
\ 'self': self}
try
let self.job = call(s:jobstart, [self.cmd, opts])
if self.job == -1
call self.error('Failed starting job: ' . string(self.cmd))
endif
catch
let self.job = -1
call self.error(['Failed starting job: ' . string(self.cmd), v:exception])
endtry
endfunc
func! yarp#core#serveraddr()
return call (s:serveraddr, [])
endfunc
func! yarp#core#error(mod, msg)
if mode() == 'i'
" NOTE: side effect, sorry, but this is necessary
set nosmd
endif
if type(a:msg) == type("")
let lines = split(a:msg, "\n", 1)
else
let lines = a:msg
endif
echoh ErrorMsg
for line in lines
echom '[' . a:mod . '@yarp] ' . line
endfor
echoh None
endfunc

View File

@ -0,0 +1,71 @@
func! yarp#pyx#init() dict
if self.type == 'py'
let l:Detect = function('s:pyexe')
else
let l:Detect = function('s:py3exe')
endif
let exe = call(l:Detect, [], self)
if get(s:, 'script', '') == ''
let s:script = globpath(&rtp,'pythonx/yarp.py',1)
endif
let self.cmd = [exe,
\ '-u',
\ s:script,
\ yarp#core#serveraddr(),
\ self.id,
\ self.module]
call self.jobstart()
endfunc
func! s:pyexe() dict
if get(g:, '_yarp_py', '')
return g:_yarp_py
endif
let g:_yarp_py = get(g:, 'python_host_prog', '')
if g:_yarp_py == '' && has('nvim') && has('python')
" heavy weight
" but better support for python detection
python import sys
let g:_yarp_py = pyeval('sys.executable')
endif
if g:_yarp_py == ''
let g:_yarp_py = 'python2'
endif
return g:_yarp_py
endfunc
func! s:py3exe() dict
if get(g:, '_yarp_py3', '')
return g:_yarp_py3
endif
let g:_yarp_py3 = get(g:, 'python3_host_prog', '')
if g:_yarp_py3 == '' && has('nvim') && has('python3')
" heavy weight
" but better support for python detection
python3 import sys
let g:_yarp_py3 = py3eval('sys.executable')
endif
if g:_yarp_py3 == ''
let g:_yarp_py3 = 'python3'
endif
if exepath(g:_yarp_py3) == ''
call self.error(
\ "Python3 executable [" .
\ g:_yarp_py3 .
\ "] not found.")
if has('vim_starting')
call self.error("")
endif
call self.error("###### Please configure g:python3_host_prog properly ######")
if has('vim_starting')
call self.error("")
endif
endif
return g:_yarp_py3
endfunc

View File

@ -0,0 +1,71 @@
from importlib.util import find_spec
if find_spec('pynvim'):
from pynvim import attach, setup_logging
else:
from neovim import attach, setup_logging
import sys
import importlib
from os import environ
assert __name__ == "__main__"
if "" in sys.path:
sys.path.remove("")
serveraddr = sys.argv[1]
yarpid = int(sys.argv[2])
module = sys.argv[3]
module_obj = None
nvim = None
environ['NVIM_YARP_MODULE'] = module
setup_logging(module)
def on_request(method, args):
if hasattr(module_obj, method):
return getattr(module_obj, method)(*args)
else:
raise Exception('method %s not found' % method)
def on_notification(method, args):
if hasattr(module_obj, method):
getattr(module_obj, method)(*args)
else:
raise Exception('method %s not found' % method)
pass
def on_setup():
pass
try:
# create another connection to avoid synchronization issue?
if len(serveraddr.split(':')) == 2:
serveraddr, port = serveraddr.split(':')
port = int(port)
nvim = attach('tcp', address=serveraddr, port=port)
else:
nvim = attach('socket', path=serveraddr)
sys.modules['vim'] = nvim
sys.modules['nvim'] = nvim
paths = nvim.eval(r'globpath(&rtp,"pythonx",1) . "\n" .'
r' globpath(&rtp,"rplugin/python3",1)')
for path in paths.split("\n"):
if not path:
continue
if path not in sys.path:
sys.path.append(path)
module_obj = importlib.import_module(module)
nvim.call('yarp#core#channel_started', yarpid, nvim.channel_id)
nvim.run_loop(on_request, on_notification, on_setup)
finally:
nvim.close()

3
bundle/vim-hug-neovim-rpc/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.pyc
__pycache__

View File

@ -0,0 +1,20 @@
Copyright © 2018 roxma@qq.com
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.

View File

@ -0,0 +1,162 @@
# vim-hug-neovim-rpc
This is an **experimental project**, trying to build a compatibility layer for
[neovim rpc client](https://github.com/neovim/python-client) working on vim8.
I started this project because I want to fix the [vim8
support](https://github.com/roxma/nvim-completion-manager/issues/14) issue for
[nvim-completion-manager](https://github.com/roxma/nvim-completion-manager).
Since this is a general purpose module, other plugins needing rpc support may
benefit from this project. However, there're many neovim rpc methods I haven't
implemented yet, which make this an experimental plugin. **Please fork and
open a PR if you get any idea on improving it**.
***Tip: for porting neovim rplugin to vim8, you might need
[roxma/nvim-yarp](https://github.com/roxma/nvim-yarp)***
![screencast](https://cloud.githubusercontent.com/assets/4538941/23102626/9e1bd928-f6e7-11e6-8fa2-2776f70819d9.gif)
## Requirements
1. vim8
2. If `has('pythonx')` and `set pyxversion=3`
- same requirements as `4. has('python3')`
2. Else if `has('pythonx')` and `set pyxversion=2`
- same requirements as `5. has('python')`
4. Else if `has('python3')`
- [pynvim](https://github.com/neovim/pynvim)
- Pynvim is normally installed by `:py3 import pip; pip.main(['install',
'--user', 'pynvim'])` or `python3 -m pip install pynvim`.
- There should be no error for at least one of `:python3 import pynvim` and
`:python3 import neovim`
5. Else if `has('python')`
- [pynvim](https://github.com/neovim/pynvim)
- Pynvim is normally installed by `:py import pip; pip.main(['install',
'--user', 'pynvim'])` or `python2 -m pip install pynvim`.
- There should be no error for at least one of `:python3 import pynvim` and
`:python3 import neovim`
6. `set encoding=utf-8` in your vimrc.
***Use `:echo neovim_rpc#serveraddr()` to test the installation***. It should print
something like `127.0.0.1:51359` or `/tmp/vmrUX9X/2`.
## API
| Function | Similar to neovim's |
|----------------------------------------------|------------------------------------------------|
| `neovim_rpc#serveraddr()` | `v:servername` |
| `neovim_rpc#jobstart(cmd,...)` | `jobstart({cmd}[, {opts}])` |
| `neovim_rpc#jobstop(jobid)` | `jobstop({job})` |
| `neovim_rpc#rpcnotify(channel,event,...)` | `rpcnotify({channel}, {event}[, {args}...])` |
| `neovim_rpc#rpcrequest(channel, event, ...)` | `rpcrequest({channel}, {method}[, {args}...])` |
Note that `neovim_rpc#jobstart` only support these options:
- `on_stdout`
- `on_stderr`
- `on_exit`
- `detach`
## Incompatibility issues
- Cannot pass `Funcref` object to python client. Pass function name instead.
- Python `None` will be converted to `''` instead of `v:null` into vimscript.
See [vim#2246](https://github.com/vim/vim/issues/2246)
- The following neovim-only API will be ignored quietly:
- `nvim_buf_add_highlight`
- `nvim_buf_clear_highlight`
## Overall Implementation
```
"vim-hug-neovim-rpc - Sequence Diagram"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┌───┐ ┌──────────┐ ┌───────────┐ ┌──────┐
│VIM│ │VIM Server│ │NVIM Server│ │Client│
└─┬─┘ └────┬─────┘ └─────┬─────┘ └──┬───┘
│ Launch thread │ │ │
│───────────────────> │ │
│ │ │ │
│ Launch thread │ │
│─────────────────────────────────────────────────>│ │
│ │ │ │
`ch_open` connect │ │ │
│───────────────────> │ │
│ │ │ │
│ │────┐ │ │
│ │ │ Launch VimHandler thread│ │
│ │<───┘ │ │
│ │ │ │
│ │ │ Connect │
│ │ │<─────────────────────────────
│ │ │ │
│ │ ────┐
│ │ │ Launch NvimHandler thread
│ │ <───┘
│ │ │ │
│ │ │ Request (msgpack rpc) │
│ │ │<─────────────────────────────
│ │ │ │
│ │ ────┐ │
│ │ │ Request enqueue │
│ │ <───┘ │
│ │ │ │
│ │ notify (method call) │ │
│ │ <────────────────────────────│ │
│ │ │ │
│ notify (json rpc) │ │ │
<─────────────────── │ │
│ │ │ │
────┐ │ │
│ Request dequeue │ │
<───┘ │ │
│ │ │ │
────┐ │ │ │
│ Process │ │ │
<───┘ │ │ │
│ │ │ │
│ │ Send response (msgpack rpc) │
│────────────────────────────────────────────────────────────────────────────────>
┌─┴─┐ ┌────┴─────┐ ┌─────┴─────┐ ┌──┴───┐
│VIM│ │VIM Server│ │NVIM Server│ │Client│
└───┘ └──────────┘ └───────────┘ └──────┘
```
<!--
@startuml
title "vim-hug-neovim-rpc - Sequence Diagram"
VIM -> "VIM Server": Launch thread
VIM -> "NVIM Server": Launch thread
VIM -> "VIM Server": `ch_open` connect
"VIM Server" -> "VIM Server": Launch VimHandler thread
Client-> "NVIM Server": Connect
"NVIM Server" -> "NVIM Server": Launch NvimHandler thread
Client -> "NVIM Server": Request (msgpack rpc)
"NVIM Server" -> "NVIM Server": Request enqueue
"NVIM Server" -> "VIM Server": notify (method call)
"VIM Server" -> VIM: notify (json rpc)
VIM -> VIM: Request dequeue
VIM -> VIM: Process
VIM -> Client: Send response (msgpack rpc)
@enduml
-->
## Debugging
Add logging settigns to your vimrc. Log files will be generated with prefix
`/tmp/nvim_log`. An alternative is to export environment variables before
starting vim/nvim.
```vim
let $NVIM_PYTHON_LOG_FILE="/tmp/nvim_log"
let $NVIM_PYTHON_LOG_LEVEL="DEBUG"
```

View File

@ -0,0 +1,204 @@
if has('pythonx')
let g:neovim_rpc#py = 'pythonx'
let s:pyeval = function('pyxeval')
elseif has('python3')
let g:neovim_rpc#py = 'python3'
let s:pyeval = function('py3eval')
else
let g:neovim_rpc#py = 'python'
let s:pyeval = function('pyeval')
endif
func! s:py(cmd)
execute g:neovim_rpc#py a:cmd
endfunc
func! neovim_rpc#serveraddr()
if exists('g:_neovim_rpc_nvim_server')
return g:_neovim_rpc_nvim_server
endif
if &encoding !=? "utf-8"
throw '[vim-hug-neovim-rpc] requires `:set encoding=utf-8`'
endif
try
call s:py('import pynvim')
catch
try
call s:py('import neovim')
catch
call neovim_rpc#_error("failed executing: " .
\ g:neovim_rpc#py . " import [pynvim|neovim]")
call neovim_rpc#_error(v:exception)
throw '[vim-hug-neovim-rpc] requires one of `:' . g:neovim_rpc#py .
\ ' import [pynvim|neovim]` command to work'
endtry
endtry
call s:py('import neovim_rpc_server')
let l:servers = s:pyeval('neovim_rpc_server.start()')
let g:_neovim_rpc_nvim_server = l:servers[0]
let g:_neovim_rpc_vim_server = l:servers[1]
let g:_neovim_rpc_main_channel = ch_open(g:_neovim_rpc_vim_server)
" identify myself
call ch_sendexpr(g:_neovim_rpc_main_channel,'neovim_rpc_setup')
return g:_neovim_rpc_nvim_server
endfunc
" elegant python function call wrapper
func! neovim_rpc#pyxcall(func,...)
call s:py('import vim, json')
let g:neovim_rpc#_tmp_args = copy(a:000)
let l:ret = s:pyeval(a:func . '(*vim.vars["neovim_rpc#_tmp_args"])')
unlet g:neovim_rpc#_tmp_args
return l:ret
endfunc
" supported opt keys:
" - on_stdout
" - on_stderr
" - on_exit
" - detach
func! neovim_rpc#jobstart(cmd,...)
let l:opts = {}
if len(a:000)
let l:opts = a:1
endif
let l:opts['_close'] = 0
let l:opts['_exit'] = 0
let l:real_opts = {'mode': 'raw'}
if has_key(l:opts,'detach') && l:opts['detach']
let l:real_opts['stoponexit'] = ''
endif
if has_key(l:opts,'on_stdout')
let l:real_opts['out_cb'] = function('neovim_rpc#_on_stdout')
endif
if has_key(l:opts,'on_stderr')
let l:real_opts['err_cb'] = function('neovim_rpc#_on_stderr')
endif
let l:real_opts['exit_cb'] = function('neovim_rpc#_on_exit')
let l:real_opts['close_cb'] = function('neovim_rpc#_on_close')
let l:job = job_start(a:cmd, l:real_opts)
let l:jobid = ch_info(l:job)['id']
let g:_neovim_rpc_jobs[l:jobid] = {'cmd':a:cmd, 'opts': l:opts, 'job': l:job}
return l:jobid
endfunc
func! neovim_rpc#jobstop(jobid)
let l:job = g:_neovim_rpc_jobs[a:jobid]['job']
return job_stop(l:job)
endfunc
func! neovim_rpc#rpcnotify(channel,event,...)
call neovim_rpc#pyxcall('neovim_rpc_server.rpcnotify',a:channel,a:event,a:000)
endfunc
let s:rspid = 1
func! neovim_rpc#rpcrequest(channel, event, ...)
let s:rspid = s:rspid + 1
" a unique key for storing response
let rspid = '' . s:rspid
" neovim's rpcrequest doesn't have timeout
let opt = {'timeout': 24 * 60 * 60 * 1000}
let args = ['rpcrequest', a:channel, a:event, a:000, rspid]
call ch_evalexpr(g:_neovim_rpc_main_channel, args, opt)
let expr = 'neovim_rpc_server.responses.pop("' . rspid . '")'
call s:py('import neovim_rpc_server, json')
let [err, result] = s:pyeval(expr)
if err
if type(err) == type('')
throw err
endif
throw err[1]
endif
return result
endfunc
func! neovim_rpc#_on_stdout(job,data)
let l:jobid = ch_info(a:job)['id']
let l:opts = g:_neovim_rpc_jobs[l:jobid]['opts']
" convert to neovim style function call
call call(l:opts['on_stdout'],[l:jobid,split(a:data,"\n",1),'stdout'],l:opts)
endfunc
func! neovim_rpc#_on_stderr(job,data)
let l:jobid = ch_info(a:job)['id']
let l:opts = g:_neovim_rpc_jobs[l:jobid]['opts']
" convert to neovim style function call
call call(l:opts['on_stderr'],[l:jobid,split(a:data,"\n",1),'stderr'],l:opts)
endfunc
func! neovim_rpc#_on_exit(job,status)
let l:jobid = ch_info(a:job)['id']
let l:opts = g:_neovim_rpc_jobs[l:jobid]['opts']
let l:opts['_exit'] = 1
" cleanup when both close_cb and exit_cb is called
if l:opts['_close'] && l:opts['_exit']
unlet g:_neovim_rpc_jobs[l:jobid]
endif
if has_key(l:opts, 'on_exit')
" convert to neovim style function call
call call(l:opts['on_exit'],[l:jobid,a:status,'exit'],l:opts)
endif
endfunc
func! neovim_rpc#_on_close(job)
let l:jobid = ch_info(a:job)['id']
let l:opts = g:_neovim_rpc_jobs[l:jobid]['opts']
let l:opts['_close'] = 1
" cleanup when both close_cb and exit_cb is called
if l:opts['_close'] && l:opts['_exit']
unlet g:_neovim_rpc_jobs[l:jobid]
endif
endfunc
func! neovim_rpc#_callback()
execute g:neovim_rpc#py . ' neovim_rpc_server.process_pending_requests()'
endfunc
let g:_neovim_rpc_main_channel = -1
let g:_neovim_rpc_jobs = {}
let s:leaving = 0
func! neovim_rpc#_error(msg)
if mode() == 'i'
" NOTE: side effect, sorry, but this is necessary
set nosmd
endif
echohl ErrorMsg
echom '[vim-hug-neovim-rpc] ' . a:msg
echohl None
endfunc
func! neovim_rpc#_nvim_err_write(msg)
if mode() == 'i'
" NOTE: side effect, sorry, but this is necessary
set nosmd
endif
echohl ErrorMsg
let g:error = a:msg
echom a:msg
echohl None
endfunc
func! neovim_rpc#_nvim_out_write(msg)
echom a:msg
endfunc

View File

@ -0,0 +1,194 @@
# vim:set et sw=4 ts=8:
import vim
# vim's python binding doesn't have the `call` method, wrap it here
def nvim_call_function(method, args):
vim.vars['_neovim_rpc_tmp_args'] = args
# vim.eval('getcurpos()') return an array of string, it should be an array
# of int. Use json_encode to workaround this
return vim.bindeval('call("%s",g:_neovim_rpc_tmp_args)' % method)
def nvim_get_current_buf():
return vim.current.buffer
def nvim_list_bufs():
return list(vim.buffers)
def nvim_buf_get_number(buf):
return buf.number
def nvim_buf_get_name(buffer):
return buffer.name
def nvim_get_var(name):
return vim.vars[name]
def nvim_get_vvar(name):
return vim.vvars[name]
def nvim_set_var(name, val):
vim.vars[name] = val
return val
def nvim_buf_get_var(buffer, name):
return buffer.vars[name]
def nvim_buf_set_var(buffer, name, val):
buffer.vars[name] = val
def nvim_buf_get_lines(buffer, start, end, *args):
if start < 0:
start = len(buffer) + 1 + start
if end < 0:
end = len(buffer) + 1 + end
return buffer[start:end]
def nvim_eval(expr):
return nvim_call_function('eval', [expr])
def nvim_buf_set_lines(buffer, start, end, err, lines):
if start < 0:
start = len(buffer) + 1 + start
if end < 0:
end = len(buffer) + 1 + end
buffer[start:end] = lines
if nvim_call_function('bufwinnr', [buffer.number]) != -1:
# vim needs' redraw to update the screen, it seems to be a bug
vim.command('redraw')
buffer_set_lines = nvim_buf_set_lines
def buffer_line_count(buffer):
return len(buffer)
def nvim_buf_line_count(buffer):
return len(buffer)
def nvim_get_option(name):
return vim.options[name]
def nvim_buf_get_option(buf, name):
return buf.options[name]
def nvim_set_option(name, val):
vim.options[name] = val
def nvim_buf_set_option(buf, name, val):
buf.options[name] = val
def nvim_command(cmd):
vim.command(cmd)
def nvim_get_current_line():
return vim.current.line
def nvim_get_current_win():
return vim.current.window
def nvim_win_get_cursor(window):
return window.cursor
def nvim_win_get_buf(window):
return window.buffer
def nvim_win_get_width(window):
return window.width
def nvim_win_set_width(window, width):
window.width = width
def nvim_win_get_height(window):
return window.height
def nvim_win_set_height(window, height):
window.height = height
def nvim_win_get_var(window, name):
return window.vars[name]
def nvim_win_set_var(window, name, val):
window.vars[name] = val
def nvim_win_get_option(window, name):
return window.options[name]
def nvim_win_set_option(window, name, val):
window.options[name] = val
def nvim_win_get_position(window):
return (window.row, window.col)
def nvim_win_get_number(window):
return window.number
def nvim_win_is_valid(window):
return window.valid
def nvim_out_write(s):
nvim_call_function('neovim_rpc#_nvim_out_write', [s])
def nvim_err_write(s):
nvim_call_function('neovim_rpc#_nvim_err_write', [s])
def nvim_buf_add_highlight(buf, src_id, *args):
# https://github.com/autozimu/LanguageClient-neovim/pull/151#issuecomment-339198527
# FIXME
return src_id
def nvim_buf_clear_highlight(*args):
# https://github.com/autozimu/LanguageClient-neovim/pull/151#issuecomment-339198527
# FIXME
pass
def nvim_set_client_info(*args):
# https://github.com/roxma/vim-hug-neovim-rpc/issues/61
vim.vars['_neovim_rpc_client_info'] = args
def nvim_get_client_info():
if '_neovim_rpc_client_info' not in vim.vars:
return []
return vim.vars['_neovim_rpc_client_info']

View File

@ -0,0 +1,69 @@
import sys
import vim
import msgpack
import neovim_rpc_server_api_info
BUFFER_TYPE = type(vim.current.buffer)
BUFFER_TYPE_ID = neovim_rpc_server_api_info.API_INFO['types']['Buffer']['id']
WINDOW_TYPE = type(vim.current.window)
WINDOW_TYPE_ID = neovim_rpc_server_api_info.API_INFO['types']['Window']['id']
def decode_if_bytes(obj):
if isinstance(obj, bytes):
return obj.decode("utf-8")
return obj
def walk(fn, obj):
if type(obj) in [list, tuple, vim.List]:
return list(walk(fn, o) for o in obj)
if type(obj) in [dict, vim.Dictionary]:
return dict((walk(fn, k), walk(fn, v)) for k, v in
obj.items())
return fn(obj)
if vim.eval("has('patch-8.0.1280')"):
def from_client(msg):
def handler(obj):
if type(obj) is msgpack.ExtType:
if obj.code == BUFFER_TYPE_ID:
return vim.buffers[msgpack.unpackb(obj.data)]
if obj.code == WINDOW_TYPE_ID:
return vim.windows[msgpack.unpackb(obj.data) - 1]
if sys.version_info.major != 2:
# python3 needs decode
obj = decode_if_bytes(obj)
return obj
return walk(handler, msg)
else:
def from_client(msg):
def handler(obj):
if type(obj) is msgpack.ExtType:
if obj.code == BUFFER_TYPE_ID:
return vim.buffers[msgpack.unpackb(obj.data)]
if obj.code == WINDOW_TYPE_ID:
return vim.windows[msgpack.unpackb(obj.data) - 1]
elif obj is None:
return ''
if sys.version_info.major != 2:
# python3 needs decode
obj = decode_if_bytes(obj)
return obj
return walk(handler, msg)
def to_client(msg):
def handler(obj):
if type(obj) == BUFFER_TYPE:
return msgpack.ExtType(BUFFER_TYPE_ID, msgpack.packb(obj.number))
if type(obj) == WINDOW_TYPE:
return msgpack.ExtType(WINDOW_TYPE_ID, msgpack.packb(obj.number))
if type(obj) == vim.Function:
try:
return obj.name.encode()
except Exception:
return ""
return obj
return walk(handler, msg)

View File

@ -0,0 +1,474 @@
# vim:set et sw=4 ts=8:
import json
import socket
import sys
import os
import threading
import vim
import logging
import msgpack
import neovim_rpc_server_api_info
import neovim_rpc_methods
import neovim_rpc_protocol
vim_error = vim.Function('neovim_rpc#_error')
vim_py = vim.eval('g:neovim_rpc#py')
if sys.version_info.major == 2:
from Queue import Queue, Empty as QueueEmpty
else:
from queue import Queue, Empty as QueueEmpty
# NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO vim test.md
try:
# Python 3
import socketserver
except ImportError:
# Python 2
import SocketServer as socketserver
# globals
logger = logging.getLogger(__name__)
# supress the annoying error message:
# No handlers could be found for logger "neovim_rpc_server"
logger.addHandler(logging.NullHandler())
request_queue = Queue()
responses = {}
def _channel_id_new():
with _channel_id_new._lock:
_channel_id_new._counter += 1
return _channel_id_new._counter
# static local
_channel_id_new._counter = 0
_channel_id_new._lock = threading.Lock()
class VimHandler(socketserver.BaseRequestHandler):
_lock = threading.Lock()
_sock = None
@classmethod
def notify(cls, cmd=None):
try:
if cmd is None:
cmd = vim_py + " neovim_rpc_server.process_pending_requests()"
if not VimHandler._sock:
return
with VimHandler._lock:
encoded = json.dumps(['ex', cmd])
logger.info("sending notification: %s", encoded)
VimHandler._sock.send(encoded.encode('utf-8'))
except Exception as ex:
logger.exception(
'VimHandler notify exception for [%s]: %s', cmd, ex)
@classmethod
def notify_exited(cls, channel):
try:
cls.notify("call neovim_rpc#_on_exit(%s)" % channel)
except Exception as ex:
logger.exception(
'notify_exited for channel [%s] exception: %s', channel, ex)
# each connection is a thread
def handle(self):
logger.info("=== socket opened ===")
data = None
while True:
try:
rcv = self.request.recv(4096)
# 16k buffer by default
if data:
data += rcv
else:
data = rcv
except socket.error:
logger.info("=== socket error ===")
break
except IOError:
logger.info("=== socket closed ===")
break
if len(rcv) == 0:
logger.info("=== socket closed ===")
break
logger.info("received: %s", data)
try:
decoded = json.loads(data.decode('utf-8'))
except ValueError:
logger.exception("json decoding failed, wait for more data")
continue
data = None
# Send a response if the sequence number is positive.
# Negative numbers are used for "eval" responses.
if (len(decoded) >= 2 and decoded[0] >= 0 and
decoded[1] == 'neovim_rpc_setup'):
VimHandler._sock = self.request
# initial setup
encoded = json.dumps(['ex', "call neovim_rpc#_callback()"])
logger.info("sending {0}".format(encoded))
self.request.send(encoded.encode('utf-8'))
else:
# recognize as rpcrequest
reqid = decoded[0]
channel = decoded[1][1]
event = decoded[1][2]
args = decoded[1][3]
rspid = decoded[1][4]
NvimHandler.request(self.request,
channel,
reqid,
event,
args,
rspid)
class SocketToStream():
def __init__(self, sock):
self._sock = sock
def read(self, cnt):
if cnt > 4096:
cnt = 4096
return self._sock.recv(cnt)
def write(self, w):
return self._sock.send(w)
class NvimHandler(socketserver.BaseRequestHandler):
channel_sockets = {}
def handle(self):
logger.info("=== socket opened for client ===")
channel = _channel_id_new()
sock = self.request
chinfo = dict(sock=sock)
NvimHandler.channel_sockets[channel] = chinfo
try:
f = SocketToStream(sock)
unpacker = msgpack.Unpacker(f)
for unpacked in unpacker:
logger.info("unpacked: %s", unpacked)
# response format:
# - msg[0]: 1
# - msg[1]: the request id
# - msg[2]: error(if any), format: [code,str]
# - msg[3]: result(if not errored)
if int(unpacked[0]) == 1:
unpacked = neovim_rpc_protocol.from_client(unpacked)
reqid = int(unpacked[1])
rspid, vimsock = chinfo[reqid]
err = unpacked[2]
result = unpacked[3]
# VIM fails to parse response when there a sleep in neovim
# client. I cannot figure out why. Use global responses to
# workaround this issue.
responses[rspid] = [err, result]
content = [reqid, '']
tosend = json.dumps(content)
# vimsock.send
vimsock.send(tosend.encode('utf-8'))
chinfo.pop(reqid)
continue
request_queue.put((f, channel, unpacked))
# notify vim in order to process request in main thread, and
# avoiding the stupid json protocol
VimHandler.notify()
logger.info('channel %s closed.', channel)
except Exception:
logger.exception('unpacker failed.')
finally:
try:
NvimHandler.channel_sockets.pop(channel)
sock.close()
except Exception:
pass
@classmethod
def notify(cls, channel, event, args):
try:
channel = int(channel)
if channel not in cls.channel_sockets:
logger.info("channel[%s] not in NvimHandler", channel)
return
sock = cls.channel_sockets[channel]['sock']
# notification format:
# - msg[0] type, which is 2
# - msg[1] method
# - msg[2] arguments
content = [2, event, args]
logger.info("notify channel[%s]: %s", channel, content)
packed = msgpack.packb(neovim_rpc_protocol.to_client(content))
sock.send(packed)
except Exception as ex:
logger.exception("notify failed: %s", ex)
@classmethod
def request(cls, vimsock, channel, reqid, event, args, rspid):
try:
reqid = int(reqid)
channel = int(channel)
chinfo = cls.channel_sockets[channel]
if channel not in cls.channel_sockets:
logger.info("channel[%s] not in NvimHandler", channel)
return
sock = chinfo['sock']
# request format:
# - msg[0] type, which is 0
# - msg[1] request id
# - msg[2] method
# - msg[3] arguments
content = [0, reqid, event, args]
chinfo[reqid] = [rspid, vimsock]
logger.info("request channel[%s]: %s", channel, content)
packed = msgpack.packb(neovim_rpc_protocol.to_client(content))
sock.send(packed)
except Exception as ex:
logger.exception("request failed: %s", ex)
@classmethod
def shutdown(cls):
# close all sockets
for channel in list(cls.channel_sockets.keys()):
chinfo = cls.channel_sockets.get(channel, None)
if chinfo:
sock = chinfo['sock']
logger.info("closing client %s", channel)
# if don't shutdown the socket, vim will never exit because the
# recv thread is still blocking
sock.shutdown(socket.SHUT_RDWR)
sock.close()
# copied from neovim python-client/neovim/__init__.py
def _setup_logging(name):
"""Setup logging according to environment variables."""
logger = logging.getLogger(__name__)
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
major_version = sys.version_info[0]
logfile = '{}_py{}_{}'.format(prefix, major_version, name)
handler = logging.FileHandler(logfile, 'w', encoding='utf-8')
handler.formatter = logging.Formatter(
'%(asctime)s [%(levelname)s @ '
'%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
logging.root.addHandler(handler)
level = logging.INFO
if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
lv = getattr(logging,
os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
level)
if isinstance(lv, int):
level = lv
logger.setLevel(level)
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
if sys.platform in ['linux', 'darwin']:
class ThreadedUnixServer(socketserver.ThreadingMixIn,
socketserver.UnixStreamServer):
pass
has_unix = True
else:
has_unix = False
def start():
_setup_logging('neovim_rpc_server')
# 0 for random port
global _vim_server
global _nvim_server
_vim_server = ThreadedTCPServer(("127.0.0.1", 0), VimHandler)
_vim_server.daemon_threads = True
vim_server_addr = "{addr[0]}:{addr[1]}".format(
addr=_vim_server.server_address)
if 'NVIM_LISTEN_ADDRESS' in os.environ:
nvim_server_addr = os.environ['NVIM_LISTEN_ADDRESS']
if nvim_server_addr.find(':') >= 0:
host, port = nvim_server_addr.split(':')
port = int(port)
_nvim_server = ThreadedTCPServer((host, port), NvimHandler)
elif has_unix:
if os.path.exists(nvim_server_addr):
try:
os.unlink(nvim_server_addr)
except Exception:
pass
_nvim_server = ThreadedUnixServer(nvim_server_addr, NvimHandler)
else:
# FIXME named pipe server ?
_nvim_server = ThreadedTCPServer(("127.0.0.1", 0), NvimHandler)
nvim_server_addr = "{addr[0]}:{addr[1]}".format(
addr=_nvim_server.server_address)
elif not has_unix:
_nvim_server = ThreadedTCPServer(("127.0.0.1", 0), NvimHandler)
nvim_server_addr = "{addr[0]}:{addr[1]}".format(
addr=_nvim_server.server_address)
else:
nvim_server_addr = vim.eval('tempname()')
_nvim_server = ThreadedUnixServer(nvim_server_addr, NvimHandler)
_nvim_server.daemon_threads = True
# Start a thread with the server -- that thread will then start one
# more thread for each request
main_server_thread = threading.Thread(target=_vim_server.serve_forever)
clients_server_thread = threading.Thread(target=_nvim_server.serve_forever)
# Exit the server thread when the main thread terminates
main_server_thread.daemon = True
main_server_thread.start()
clients_server_thread.daemon = True
clients_server_thread.start()
return [nvim_server_addr, vim_server_addr]
def process_pending_requests():
logger.info("process_pending_requests")
while True:
item = None
try:
item = request_queue.get(False)
f, channel, msg = item
msg = neovim_rpc_protocol.from_client(msg)
logger.info("get msg from channel [%s]: %s", channel, msg)
# request format:
# - msg[0] type, which is 0
# - msg[1] request id
# - msg[2] method
# - msg[3] arguments
# notification format:
# - msg[0] type, which is 2
# - msg[1] method
# - msg[2] arguments
if msg[0] == 0:
# request
req_typed, req_id, method, args = msg
try:
err = None
result = _process_request(channel, method, args)
except Exception as ex:
logger.exception("process failed: %s", ex)
# error uccor
err = [1, str(ex)]
result = None
result = [1, req_id, err, result]
logger.info("sending result: %s", result)
packed = msgpack.packb(neovim_rpc_protocol.to_client(result))
f.write(packed)
logger.info("sent")
if msg[0] == 2:
# notification
req_typed, method, args = msg
try:
result = _process_request(channel, method, args)
logger.info('notification process result: [%s]', result)
except Exception as ex:
logger.exception("process failed: %s", ex)
except QueueEmpty:
pass
except Exception as ex:
logger.exception("exception during process: %s", ex)
finally:
if item:
request_queue.task_done()
else:
# item==None means the queue is empty
break
def _process_request(channel, method, args):
if hasattr(neovim_rpc_methods, method):
return getattr(neovim_rpc_methods, method)(*args)
elif method in ['vim_get_api_info', 'nvim_get_api_info']:
# the first request sent by neovim python client
return [channel, neovim_rpc_server_api_info.API_INFO]
else:
logger.error("method %s not implemented", method)
vim_error(
"rpc method [%s] not implemented in "
"pythonx/neovim_rpc_methods.py. "
"Please send PR or contact the mantainer." % method)
raise Exception('%s not implemented' % method)
def rpcnotify(channel, method, args):
NvimHandler.notify(channel, method, args)
def stop():
logger.info("stop begin")
# close tcp channel server
_nvim_server.shutdown()
_nvim_server.server_close()
# close the main channel
try:
vim.command('call ch_close(g:_neovim_rpc_main_channel)')
except Exception as ex:
logger.info("ch_close failed: %s", ex)
# remove all sockets
NvimHandler.shutdown()
try:
# stop the main channel
_vim_server.shutdown()
except Exception as ex:
logger.info("_vim_server shutodwn failed: %s", ex)
try:
_vim_server.server_close()
except Exception as ex:
logger.info("_vim_server close failed: %s", ex)

File diff suppressed because it is too large Load Diff