├── .gitignore ├── LICENSE ├── README.md ├── autoload ├── cm.vim └── cm │ ├── snippet.vim │ └── sources │ ├── css.vim │ ├── neosnippet.vim │ ├── snipmate.vim │ └── ultisnips.vim ├── doc └── nvim-completion-manager.txt ├── plugin └── cm.vim └── pythonx ├── cm.py ├── cm_core.py ├── cm_default.py ├── cm_matchers ├── abbrev_matcher.py ├── fuzzy_matcher.py ├── prefix_matcher.py └── substr_matcher.py ├── cm_scopers ├── html_scoper.py ├── markdown_scoper.py └── rst_scoper.py ├── cm_sources ├── cm_bufkeyword.py ├── cm_filepath.py ├── cm_gocode.py ├── cm_jedi.py ├── cm_keyword_continue.py ├── cm_tags.py └── cm_tmux.py └── cm_start.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /node_modules 3 | /doc/tags 4 | *.swp 5 | *.pyc 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2017 roxma@qq.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: THIS REPO IS DEPRECATED. USE [NCM2](https://github.com/ncm2/ncm2) INSTEAD. 2 | 3 | --- 4 | 5 | :heart: for my favorite editor 6 | 7 | # A Completion Framework for Neovim 8 | 9 | This is a **fast, extensible, async completion framework** for 10 | [neovim](https://github.com/neovim/neovim). For more information about plugin 11 | implementation, please read the **[Why](#why) section**. 12 | 13 | Future updates, announcements, screenshots will be posted 14 | **[here](https://github.com/roxma/nvim-completion-manager/issues/12). 15 | Subscribe it if you are interested.** 16 | 17 | ![All in one screenshot](https://cloud.githubusercontent.com/assets/4538941/23752974/8fffbdda-0512-11e7-8466-8a30f480de21.gif) 18 | 19 | 20 | ## Table of Contents 21 | 22 | 23 | 24 | * [Features](#features) 25 | * [Scoping Sources:](#scoping-sources) 26 | * [Completion Sources](#completion-sources) 27 | * [How to extend this framework?](#how-to-extend-this-framework) 28 | * [Requirements](#requirements) 29 | * [Installation](#installation) 30 | * [Optional Configuration Tips](#optional-configuration-tips) 31 | * [Why?](#why) 32 | * [Async architecture](#async-architecture) 33 | * [Scoping](#scoping) 34 | * [Experimental hacking](#experimental-hacking) 35 | * [FAQ](#faq) 36 | * [Why Python?](#why-python) 37 | * [Trouble-shooting](#trouble-shooting) 38 | * [Related Projects](#related-projects) 39 | 40 | 41 | 42 | 43 | ## Features 44 | 45 | 1. Asynchronous completion support like deoplete. 46 | 2. Faster, all completions should run in parallel. 47 | 3. Smarter on files with different languages, for example, css/javascript 48 | completion in html style/script tag. 49 | 4. Extensible async vimscript API and python3 API. 50 | 5. Function parameter expansion via 51 | [Ultisnips](https://github.com/SirVer/ultisnips), 52 | [neosnippet.vim](https://github.com/Shougo/neosnippet.vim) or 53 | [vim-snipmate](https://github.com/garbas/vim-snipmate). 54 | 55 | 56 | ## Scoping Sources: 57 | 58 | - Language specific completion for markdown, reStructuredText 59 | - Javascript code completion in html script tag 60 | - Css code completion in html style tag 61 | 62 | ## Completion Sources 63 | 64 | | Language / Description | Repository | 65 | |--------------------------|----------------------------------------------------------------------------------------| 66 | | Word from current buffer | builtin | 67 | | Word from tmux session | builtin | 68 | | ctags completion | builtin | 69 | | Filepath completion | builtin | 70 | | Python | builtin, requires [jedi](https://github.com/davidhalter/jedi) | 71 | | Css | builtin, requires [csscomplete#CompleteCSS](https://github.com/othree/csscomplete.vim) | 72 | | Golang | builtin, requires [gocode](https://github.com/nsf/gocode) | 73 | | Ultisnips hint | builtin, requires [Ultisnips](https://github.com/SirVer/ultisnips) | 74 | | Snipmate hint | builtin, requires [vim-snipmate](https://github.com/garbas/vim-snipmate) | 75 | | neosnippet hint | builtin, requires [neosnippet.vim](https://github.com/Shougo/neosnippet.vim) | 76 | | Language Server Protocol | [autozimu/LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim) | 77 | | C/C++ | [clang_complete](https://github.com/roxma/clang_complete) [DEPRECATED] | 78 | | C/C++ | [ncm-clang](https://github.com/roxma/ncm-clang) | 79 | | Javascript | [nvim-cm-tern](https://github.com/roxma/nvim-cm-tern) | 80 | | Javascript | [nvim-cm-flow](https://github.com/roxma/ncm-flow) | 81 | | elm | [ncm-elm-oracle](https://github.com/roxma/ncm-elm-oracle) | 82 | | Clojure | [clojure-async-clj-omni](https://github.com/clojure-vim/async-clj-omni) | 83 | | Rust | [nvim-cm-racer](https://github.com/roxma/nvim-cm-racer) | 84 | | Vimscript | [Shougo/neco-vim](https://github.com/Shougo/neco-vim) | 85 | | Ruby | [ncm-rct-complete](https://github.com/roxma/ncm-rct-complete) | 86 | | PHP | [LanguageServer-php-neovim](https://github.com/roxma/LanguageServer-php-neovim) | 87 | | PHP | [ncm-phpactor](https://github.com/roxma/ncm-phpactor) | 88 | | Swift | [dafufer/nvim-cm-swift-completer](https://github.com/dafufer/nvim-cm-swift-completer) | 89 | | gtags completion | [jsfaint/gen_tags.vim](https://github.com/jsfaint/gen_tags.vim) | 90 | | syntax completion | [Shougo/neco-syntax](https://github.com/Shougo/neco-syntax) | 91 | | include completion | [Shougo/neoinclude](https://github.com/Shougo/neoinclude.vim) | 92 | | github completion | [ncm-github](https://github.com/roxma/ncm-github) | 93 | | mutt mails | [#97 mutt-aliases.vim](https://github.com/roxma/nvim-completion-manager/issues/97) | 94 | | deoplete | [#50 deoplete](https://github.com/roxma/nvim-completion-manager/issues/50) | 95 | | css | [calebeby/ncm-css](https://github.com/calebeby/ncm-css) | 96 | | lbdb (addressbook) | [katsika/ncm-lbdb](https://github.com/katsika/ncm-lbdb) | 97 | | Java | [sassanh/nvim-cm-eclim](https://github.com/sassanh/nvim-cm-eclim) | 98 | | TypeScript | [mhartington/nvim-typescript](https://github.com/mhartington/nvim-typescript) | 99 | | Word from other buffers | [fgrsnau/ncm-otherbuf](https://github.com/fgrsnau/ncm-otherbuf) 100 | | R | [gaalcaras/ncm-R](https://github.com/gaalcaras/ncm-R) 101 | 102 | 103 | ## How to extend this framework? 104 | 105 | - `:help ncm-source-examples` for minimal examples 106 | - Some real, small examples: 107 | [github-emoji](https://github.com/roxma/ncm-github/blob/master/pythonx/cm_sources/github_emoji.py), 108 | [nvim-cm-racer](https://github.com/roxma/nvim-cm-racer) 109 | 110 | Please announce your new plugin 111 | [here](https://github.com/roxma/nvim-completion-manager/issues/12) after 112 | you've created the extension. 113 | 114 | 115 | ## Requirements 116 | 117 | - For Neovim: 118 | - `echo has("python3")` 119 | - For Vim 8: 120 | - [roxma/vim-hug-neovim-rpc](https://github.com/roxma/vim-hug-neovim-rpc) 121 | - `g:python3_host_prog` pointed to your python3 executable, or `echo 122 | exepath('python3')` is not empty. 123 | - [neovim python client](https://github.com/neovim/python-client) (`pip3 124 | install neovim`) 125 | 126 | ## Installation 127 | 128 | - Assuming you're using [vim-plug](https://github.com/junegunn/vim-plug) 129 | 130 | ```vim 131 | " the framework 132 | Plug 'roxma/nvim-completion-manager' 133 | ``` 134 | 135 | - If you are **vim8 user**, you'll need 136 | [vim-hug-neovim-rpc](https://github.com/roxma/vim-hug-neovim-rpc). The vim8 137 | support layer is still experimental, please 'upgrade' to 138 | [neovim](https://github.com/neovim/neovim) if it's possible. 139 | 140 | ```vim 141 | " Requires vim8 with has('python') or has('python3') 142 | " Requires the installation of msgpack-python. (pip install msgpack-python) 143 | if !has('nvim') 144 | Plug 'roxma/vim-hug-neovim-rpc' 145 | endif 146 | ``` 147 | 148 | - Install pip modules for your neovim python3: 149 | 150 | ```sh 151 | # neovim is the required pip module 152 | # jedi for python completion 153 | # psutil (optional) 154 | # setproctitle (optional) 155 | pip3 install --user neovim jedi psutil setproctitle 156 | ``` 157 | 158 | (Optional) It's easier to use 159 | [python-support.nvim](https://github.com/roxma/python-support.nvim) to help 160 | manage your pip modules for neovim: 161 | 162 | ## Optional Configuration Tips 163 | 164 | - Supress the annoying completion messages: 165 | 166 | ```vim 167 | " don't give |ins-completion-menu| messages. For example, 168 | " '-- XXX completion (YYY)', 'match 1 of 2', 'The only match', 169 | set shortmess+=c 170 | ``` 171 | 172 | - When the `` key is pressed while the popup menu is visible, it only 173 | hides the menu. Use this mapping to hide the menu and also start a new line. 174 | 175 | ```vim 176 | inoremap (pumvisible() ? "\\" : "\") 177 | ``` 178 | 179 | - Here is an example for expanding snippet in the popup menu with `` 180 | key. Suppose you use the `` key for expanding snippet. 181 | 182 | ```vim 183 | imap (pumvisible() ? "\\(expand_or_nl)" : "\") 184 | imap (expand_or_nl) (cm#completed_is_snippet() ? "\":"\") 185 | ``` 186 | 187 | - When using `CTRL-C` key to leave insert mode, it does not trigger the 188 | autocmd `InsertLeave`. You should use `CTRL-[`, or map the `` to 189 | ``. 190 | 191 | ```vim 192 | inoremap 193 | ``` 194 | 195 | - Use `` to select the popup menu: 196 | 197 | ```vim 198 | inoremap pumvisible() ? "\" : "\" 199 | inoremap pumvisible() ? "\" : "\" 200 | ``` 201 | 202 | - If 'omnifunc' is the only available option, you may register it as a source 203 | for NCM. 204 | 205 | 206 | ```vim 207 | " css completion via `csscomplete#CompleteCSS` 208 | " The `'cm_refresh_patterns'` is PCRE. 209 | " Be careful with `'scoping': 1` here, not all sources, especially omnifunc, 210 | " can handle this feature properly. 211 | au User CmSetup call cm#register_source({'name' : 'cm-css', 212 | \ 'priority': 9, 213 | \ 'scoping': 1, 214 | \ 'scopes': ['css','scss'], 215 | \ 'abbreviation': 'css', 216 | \ 'word_pattern': '[\w\-]+', 217 | \ 'cm_refresh_patterns':['[\w\-]+\s*:\s+'], 218 | \ 'cm_refresh': {'omnifunc': 'csscomplete#CompleteCSS'}, 219 | \ }) 220 | ``` 221 | 222 | **Warning:** `omnifunc` is implemented in a synchronouse style, and 223 | vim-vimscript is single threaded, it would potentially block the ui with the 224 | introduction of a heavy weight `omnifunc`, for example the builtin 225 | phpcomplete. If you get some time, please try implementing a source for NCM as 226 | a replacement for the old style omnifunc. 227 | 228 | 229 | - There's no guarantee that this plugin will be compatible with other 230 | completion plugin in the same buffer. Use `let g:cm_smart_enable=0` and 231 | `call cm#enable_for_buffer()` to use this plugin for specific buffer. 232 | 233 | - This example shows how to disable NCM's builtin tag completion. It's also 234 | possible to use `g:cm_sources_override` to override other default options of 235 | a completion source. 236 | 237 | ```vim 238 | let g:cm_sources_override = { 239 | \ 'cm-tags': {'enable':0} 240 | \ } 241 | ``` 242 | 243 | ## Why? 244 | 245 | This project was started just for fun, and it's working pleasingly for me now. 246 | However, it seems there's lots of differences between deoplete, YCM, and 247 | nvim-completion-manager, by implementation. 248 | 249 | I haven't read the source of YCM yet. So here I'm describing the basic 250 | implementation of NCM (short for nvim-completion-manager) and some of the 251 | differences between deoplete and this plugin. 252 | 253 | ### Async architecture 254 | 255 | Each completion source should be a thread or a standalone process, the manager 256 | notifies the completion source for any text changing, even when popup menu is 257 | visible. The completion source notifies the manager if there's any complete 258 | matches available. After some basic priority sorting between completion 259 | sources, and some simple filtering, the completion popup menu will be 260 | triggered with the `complete()` function by the completion manager. 261 | 262 | If some of the completion source is calculating matches for a long long time, 263 | the popup menu will still be shown quickly if other completion sources work 264 | properly. And if the user hasn't changed anything, the popup menu will be 265 | updated after the slow completion source finishes the work. 266 | 267 | As the time as of this plugin being created, the completion sources of 268 | deoplete are gathered with `gather_candidates()` of the `Source` object, 269 | inside a for loop, in deoplete's process. A slow completion source may defer 270 | the display of popup menu. Of course it will not block the ui. 271 | 272 | The good news is that deoplete has supported async `gather_candidates` now. 273 | But still, NCM is potentially faster because all completion sources run in 274 | parallel. 275 | 276 | ### Scoping 277 | 278 | I write markdown files with code blocks quite often, so I've also implemented 279 | language specific completion for markdown file. This is a framework feature, 280 | which is called scoping. It should work for any markdown code block whose 281 | language completion source is avaible to NCM. I've also added support for 282 | javascript completion in script tag of html files, and css completion in style 283 | tag. 284 | 285 | The idea was originated in 286 | [vim-syntax-compl-pop](https://github.com/roxma/vim-syntax-compl-pop). Since 287 | it's pure vimscript implementation, and there are some limitations currently 288 | with neovim's syntax api. It's very likely that vim-syntax-compl-pop doesn't 289 | work, for example, javascript completion in markdown or html script tag. So I 290 | use custom parser in NCM to implement the scoping features. 291 | 292 | ### Experimental hacking 293 | 294 | Note that there's some hacking done in NCM. It uses a per 30ms timer to detect 295 | changes even popup menu is visible, as well as using the `TextChangedI` event, 296 | which only triggers when no popup menu is visible. This is important for 297 | implementing the async architecture. I'm hoping one day neovim will offer 298 | better option rather than a timer or the limited `TextChangedI`. 299 | 300 | ## FAQ 301 | 302 | ### Why Python? 303 | 304 | YouCompleteMe has [good 305 | explanation](https://github.com/Valloric/YouCompleteMe#why-isnt-ycm-just-written-in-plain-vimscript-ffs). 306 | 307 | ## Trouble-shooting 308 | 309 | Moved to [wiki](https://github.com/roxma/nvim-completion-manager/wiki/Trouble-shooting) 310 | 311 | ## Related Projects 312 | 313 | [asyncomplete.vim](https://github.com/prabirshrestha/asyncomplete.vim) 314 | 315 | -------------------------------------------------------------------------------- /autoload/cm.vim: -------------------------------------------------------------------------------- 1 | if get(s:,'init','0') 2 | finish 3 | endif 4 | let s:init = 1 5 | let s:already_setup = 0 6 | 7 | inoremap (cm_inject_snippet) cm#snippet#check_and_inject() 8 | 9 | " use silent mapping that doesn't slower the terminal ui 10 | " Note: `:help complete()` says: 11 | " > You need to use a mapping with CTRL-R = |i_CTRL-R|. It does not work 12 | " > after CTRL-O or with an expression mapping. 13 | " 14 | " use g:cm_completekeys to decide which one to use. 15 | inoremap (cm_complete) =cm#_complete() 16 | inoremap (cm_completefunc) 17 | inoremap (cm_omnifunc) 18 | 19 | " Show the popup menu, reguardless of the matching of cm_refresh_pattern 20 | inoremap (cm_force_refresh) (cm#menu_selected()?"\\=cm#_force_refresh()\":"\=cm#_force_refresh()\") 21 | 22 | 23 | let s:rpcnotify = 'rpcnotify' 24 | let s:jobstart = 'jobstart' 25 | let s:jobstop = 'jobstop' 26 | func! s:servername() 27 | return v:servername 28 | endfunc 29 | 30 | if has('nvim')==0 31 | let s:rpcnotify = 'neovim_rpc#rpcnotify' 32 | let s:jobstart = 'neovim_rpc#jobstart' 33 | let s:jobstop = 'neovim_rpc#jobstop' 34 | func! s:servername() 35 | return neovim_rpc#serveraddr() 36 | endfunc 37 | endif 38 | 39 | 40 | func! cm#enable_for_buffer(...) 41 | 42 | let g:_cm_servername = s:servername() 43 | 44 | if s:already_setup == 0 45 | let s:already_setup = 1 46 | call cm#snippet#init() 47 | endif 48 | 49 | call s:cm_setup() 50 | 51 | let b:cm_enable = 1 52 | if len(a:000) 53 | let b:cm_enable = a:1 54 | endif 55 | 56 | " TODO this override the global options, any way to fix this? 57 | let &completeopt=g:cm_completeopt 58 | if g:cm_completekeys=="\(cm_completefunc)" 59 | set completefunc=cm#_completefunc 60 | endif 61 | if g:cm_completekeys=="\(cm_omnifunc)" 62 | set omnifunc=cm#_completefunc 63 | endif 64 | 65 | augroup cm 66 | autocmd! * 67 | autocmd InsertEnter call s:notify_core_channel('cm_insert_enter') | call s:on_insert_enter() 68 | autocmd InsertLeave call s:change_tick_stop() 69 | autocmd BufEnter let &completeopt=g:cm_completeopt 70 | " working together with timer, the timer is for detecting changes 71 | " popup menu is visible. TextChangedI will not be triggered when popup 72 | " menu is visible, but TextChangedI is more efficient and faster than 73 | " timer when popup menu is not visible. 74 | autocmd TextChangedI call s:check_changes() 75 | augroup END 76 | 77 | call s:start_core_channel() 78 | 79 | call s:check_rtp() 80 | endfunc 81 | 82 | func! cm#disable_for_buffer() 83 | let b:cm_enable = 0 84 | augroup cm 85 | autocmd! * 86 | augroup END 87 | call s:change_tick_stop() 88 | endfunc 89 | 90 | 91 | func! cm#context() 92 | let l:ret = {'bufnr':bufnr('%'), 'curpos':getcurpos(), 'changedtick':b:changedtick} 93 | let l:ret['lnum'] = l:ret['curpos'][1] 94 | let l:ret['col'] = l:ret['curpos'][2] 95 | let l:ret['filetype'] = &filetype 96 | let l:ret['filepath'] = expand('%:p') 97 | if l:ret['filepath'] == '' 98 | " this is necessary here, otherwise empty filepath is somehow 99 | " converted to None in vim's python binding. 100 | let l:ret['filepath'] = "" 101 | endif 102 | let l:ret['typed'] = strpart(getline(l:ret['lnum']),0,l:ret['col']-1) 103 | return l:ret 104 | endfunc 105 | 106 | func! cm#context_changed(ctx) 107 | " return (b:changedtick!=a:ctx['changedtick']) || (getcurpos()!=a:ctx['curpos']) 108 | " Note: changedtick is triggered when `` is pressed due to vim's 109 | " bug, use curpos as workaround 110 | return getcurpos()!=a:ctx['curpos'] 111 | endfunc 112 | 113 | func! cm#register_source(info) 114 | 115 | let l:name = a:info['name'] 116 | 117 | " if registered before, ignore this call 118 | if has_key(g:_cm_sources, l:name) 119 | return 120 | endif 121 | 122 | if has_key(g:cm_sources_override, l:name) 123 | " override source default options 124 | call extend(a:info, g:cm_sources_override[l:name]) 125 | endif 126 | 127 | let a:info['enable'] = get(a:info, 'enable', g:cm_sources_enable) 128 | 129 | " the name cm_refresh_min_word_len is deprecated, it will be removed 130 | " in the future 131 | if has_key(a:info, 'cm_refresh_min_word_len') 132 | let a:info['cm_refresh_length'] = a:info['cm_refresh_min_word_len'] 133 | endif 134 | 135 | if !has_key(a:info,'cm_refresh_length') 136 | if type(g:cm_refresh_length)==type(1) 137 | let a:info['cm_refresh_length'] = g:cm_refresh_length 138 | else 139 | " format: [ [ minimal priority, min length ], []] 140 | " 141 | " Configure by min priority level. Use the max priority setting 142 | " available 143 | let l:max = -1 144 | for l:e in g:cm_refresh_length 145 | if (a:info['priority'] >= l:e[0]) && (l:e[0] > l:max) 146 | let a:info['cm_refresh_length'] = l:e[1] 147 | let l:max = l:e[0] 148 | endif 149 | endfor 150 | endif 151 | endif 152 | 153 | " wether or not use the framework's standard sorting 154 | let a:info['sort'] = get(a:info,'sort',1) 155 | 156 | " similar to g:cm_auto_popup 157 | let a:info['auto_popup'] = get(a:info,'auto_popup',1) 158 | 159 | let a:info['early_cache'] = get(a:info,'early_cache', 0) 160 | 161 | let g:_cm_sources[l:name] = a:info 162 | 163 | call s:notify_core_channel('cm_start_channels',g:_cm_sources,cm#context()) 164 | 165 | endfunc 166 | 167 | func! cm#disable_source(name) 168 | try 169 | let l:info = g:_cm_sources[a:name] 170 | let l:info['enable'] = 0 171 | call cm#_channel_cleanup(l:info) 172 | catch 173 | echom v:exception 174 | endtry 175 | endfunc 176 | 177 | 178 | func! cm#complete(src, context, startcol, matches, ...) 179 | 180 | let l:refresh = 0 181 | if len(a:000) 182 | let l:refresh = a:1 183 | endif 184 | 185 | " ignore the request if context has changed 186 | if cm#context_changed(a:context) 187 | call s:notify_core_channel('cm_complete',g:_cm_sources,a:src,a:context,a:startcol,a:matches,l:refresh,1,cm#context()) 188 | return 1 189 | endif 190 | 191 | call s:notify_core_channel('cm_complete',g:_cm_sources,a:src,a:context,a:startcol,a:matches,l:refresh,0,'') 192 | return 0 193 | 194 | endfunc 195 | 196 | func! cm#completed_is_snippet() 197 | return cm#snippet#completed_is_snippet() 198 | endfunc 199 | 200 | " show message to user 201 | func! cm#message(msgtype, msg) 202 | if toupper(a:msgtype) == 'ERROR' 203 | if mode() == 'i' 204 | " side effect 205 | set nosmd 206 | endif 207 | echoh WarningMsg | echom '[ERROR] ' . a:msg | echoh None 208 | return 209 | endif 210 | echom '[' . toupper(a:msgtype) . '] ' . a:msg 211 | endfunc 212 | 213 | " internal functions and variables 214 | 215 | let g:_cm_sources = {} 216 | let s:leaving = 0 217 | let s:change_timer = -1 218 | let s:lasttick = [] 219 | let s:channel_jobid = -1 220 | let g:_cm_channel_id = -1 221 | let s:channel_started = 0 222 | let g:_cm_start_py_path = globpath(&rtp,'pythonx/cm_start.py',1) 223 | let s:complete_start_timer = 0 224 | let s:old_rtp = &rtp 225 | 226 | " global lock for being compatible with other plugins: 227 | " 1 - vim-multiple-cursors 228 | let g:_cm_lock = 0 229 | 230 | augroup cm 231 | autocmd! 232 | autocmd VimLeavePre * let s:leaving=1 233 | 234 | autocmd User MultipleCursorsPre let g:_cm_lock = or(g:_cm_lock, 0x1) 235 | autocmd User MultipleCursorsPost let g:_cm_lock = and(g:_cm_lock, invert(0x1)) 236 | augroup END 237 | 238 | func! s:check_scope(info) 239 | let l:scopes = get(a:info,'scopes',[]) 240 | if empty(l:scopes) 241 | " This is a general completion source 242 | return 1 243 | endif 244 | " only check the root scope 245 | let l:cur_scope = &filetype 246 | for l:scope in l:scopes 247 | if l:scope == l:cur_scope 248 | return 1 249 | endif 250 | endfor 251 | return 0 252 | endfunc 253 | 254 | func! cm#_core_channel_started(id) 255 | let g:_cm_channel_id = a:id 256 | " start channels as soon as possible 257 | call s:notify_core_channel('cm_start_channels',g:_cm_sources,cm#context()) 258 | endfunc 259 | 260 | func! cm#_channel_started(name,id) 261 | 262 | if !has_key(g:_cm_sources,a:name) 263 | return 264 | endif 265 | 266 | let l:channel = g:_cm_sources[a:name]['channel'] 267 | let l:channel['id'] = a:id 268 | 269 | " register events 270 | execute 'augroup cm_channel_' . a:id 271 | for l:event in get(l:channel,'events',[]) 272 | let l:exec = 'if get(b:,"cm_enable",0) | silent! call call(s:rpcnotify,[' . a:id . ', "cm_event", "'.l:event.'",cm#context()]) | endif' 273 | if type(l:event)==type('') 274 | execute 'au ' . l:event . ' * ' . l:exec 275 | elseif type(l:event)==type([]) 276 | execute 'au ' . join(l:event,' ') .' ' . l:exec 277 | endif 278 | endfor 279 | execute 'augroup END' 280 | 281 | " refresh for this channel 282 | call s:on_changed() 283 | 284 | endfunc 285 | 286 | func! cm#_channel_cleanup(info) 287 | 288 | if !has_key(a:info,'channel') 289 | return 290 | endif 291 | 292 | let l:channel = a:info['channel'] 293 | 294 | if has_key(l:channel,'id') 295 | " clean event group 296 | execute 'augroup cm_channel_' . l:channel['id'] 297 | execute 'autocmd!' 298 | execute 'augroup END' 299 | unlet l:channel['id'] 300 | endif 301 | 302 | let l:channel['has_terminated'] = 1 303 | 304 | endfunc 305 | 306 | func! cm#_core_complete(context, startcol, matches, not_changed, snippets) 307 | 308 | if s:should_skip() 309 | return 310 | endif 311 | 312 | " ignore the request if context has changed 313 | if cm#context_changed(a:context) 314 | return 315 | endif 316 | 317 | if a:not_changed && pumvisible() 318 | return 319 | endif 320 | 321 | " from core channel 322 | " something selected by user, do not refresh the menu 323 | if cm#menu_selected() 324 | return 325 | endif 326 | 327 | let s:context = a:context 328 | let s:startcol = a:startcol 329 | let l:padcmd = 'extend(v:val,{"abbr":printf("%".strdisplaywidth(v:val["padding"])."s%s","",v:val["abbr"])})' 330 | let s:matches = map(a:matches, l:padcmd) 331 | let g:cm#snippet#snippets = a:snippets 332 | 333 | call feedkeys(g:cm_completekeys, 'i') 334 | 335 | endfunc 336 | 337 | func! cm#_completefunc(findstart,base) 338 | if a:findstart 339 | return s:startcol-1 340 | endif 341 | return {'refresh': 'always', 'words': s:matches } 342 | endfunc 343 | 344 | func! cm#_complete() 345 | call complete(s:startcol, s:matches) 346 | return '' 347 | endfunc 348 | 349 | func! cm#_force_refresh() 350 | " force=1 351 | call s:notify_core_channel('cm_refresh',g:_cm_sources,cm#context(),1) 352 | return '' 353 | endfunc 354 | 355 | " cm core channel functions 356 | " { 357 | 358 | 359 | function! s:on_core_channel_error(job_id, data, event) 360 | echoe join(a:data,"\n") 361 | endfunction 362 | 363 | func! s:start_core_channel(...) 364 | if s:channel_started 365 | return 366 | endif 367 | let s:channel_started = 1 368 | 369 | let g:_cm_py3 = get(g:,'python3_host_prog','') 370 | if g:_cm_py3 == '' && has('nvim') && has('python3') 371 | " heavy weight 372 | " but better support for python detection 373 | python3 import sys 374 | let g:_cm_py3 = py3eval('sys.executable') 375 | endif 376 | if g:_cm_py3 == '' 377 | let g:_cm_py3 = 'python3' 378 | endif 379 | 380 | let s:channel_jobid = call(s:jobstart,[[g:_cm_py3, g:_cm_start_py_path, 'core', g:_cm_servername], { 381 | \ 'on_exit' : function('s:on_core_channel_exit'), 382 | \ 'on_stderr' : function('s:on_core_channel_error'), 383 | \ 'detach' : 1, 384 | \ }]) 385 | endfunc 386 | 387 | fun s:on_core_channel_exit(job_id, data, event) 388 | let s:channel_jobid = -1 389 | if s:leaving || v:dying 390 | return 391 | endif 392 | " v:exiting is not availale on vim8 393 | if get(v:, 'exiting', v:null) isnot v:null 394 | return 395 | endif 396 | echom 'nvim-completion-manager core channel terminated. ' 397 | endf 398 | 399 | fun s:notify_core_channel(event,...) 400 | " if s:channel_jobid==-1 401 | if g:_cm_channel_id==-1 402 | return -1 403 | endif 404 | " forward arguments 405 | call call(s:rpcnotify,[g:_cm_channel_id, a:event] + a:000 ) 406 | return 0 407 | endf 408 | " } 409 | 410 | func! s:cm_setup() 411 | " avoid the message 'No matching autocommands' for doautocmd 412 | autocmd User CmSetup silent 413 | doautocmd User CmSetup 414 | " remove executed CmSetup, so that when the User CmSetup is lazy 415 | " registered, we can doautocmd again 416 | autocmd! User CmSetup 417 | endfunc 418 | 419 | func! s:changetick() 420 | " return [b:changedtick , getcurpos()] 421 | " Note: changedtick is triggered when `` is pressed due to vim's 422 | " bug, use curpos as workaround 423 | return getcurpos() 424 | endfunc 425 | 426 | func! s:should_skip() 427 | return !get(b:,'cm_enable',0) || &paste!=0 || g:_cm_lock || mode() != 'i' 428 | endfunc 429 | 430 | func! s:check_rtp() 431 | if s:old_rtp != &rtp 432 | let s:old_rtp = &rtp 433 | call s:cm_setup() 434 | call s:notify_core_channel('cm_detect_modules') 435 | endif 436 | endfunc 437 | 438 | func! s:on_insert_enter() 439 | call s:check_rtp() 440 | 441 | if s:change_timer!=-1 442 | return 443 | endif 444 | let s:lasttick = s:changetick() 445 | " check changes every 30ms, which is 0.03s, it should be fast enough 446 | let s:change_timer = timer_start(30,function('s:check_changes'),{'repeat':-1}) 447 | 448 | call s:on_changed() 449 | endfunc 450 | 451 | func! s:change_tick_stop() 452 | if s:change_timer==-1 453 | return 454 | endif 455 | call timer_stop(s:change_timer) 456 | let s:lasttick = [] 457 | let s:change_timer = -1 458 | endfunc 459 | 460 | 461 | func! s:check_changes(...) 462 | 463 | if s:should_skip() 464 | return 465 | endif 466 | 467 | let l:tick = s:changetick() 468 | 469 | if l:tick!=s:lasttick 470 | let s:lasttick = l:tick 471 | 472 | if g:cm_complete_start_delay == 0 473 | call s:on_changed() 474 | else 475 | if s:complete_start_timer 476 | call timer_stop(s:complete_start_timer) 477 | endif 478 | let s:complete_start_timer = timer_start(g:cm_complete_start_delay, function('s:complete_start_timer_handler'), {'repeat': 1}) 479 | endif 480 | endif 481 | 482 | call cm#snippet#check_and_inject() 483 | endfunc 484 | 485 | func! s:complete_start_timer_handler(...) 486 | let s:complete_start_timer = 0 487 | call s:on_changed() 488 | endfunc 489 | 490 | " on completion context changed 491 | func! s:on_changed() 492 | 493 | if s:should_skip() 494 | return 495 | endif 496 | 497 | if g:cm_auto_popup==0 498 | return 499 | endif 500 | 501 | call s:notify_core_channel('cm_refresh',g:_cm_sources,cm#context(),0) 502 | endfunc 503 | 504 | func! cm#_auto_enable_check(...) 505 | if exists('b:cm_enable') && b:cm_enable!=2 506 | return 507 | endif 508 | if (&buftype=='' && line2byte(line("$") + 1) and , cm#menu_selected 568 | " will not work. 569 | return pumvisible() && !empty(v:completed_item) 570 | endfunc 571 | 572 | -------------------------------------------------------------------------------- /autoload/cm/snippet.vim: -------------------------------------------------------------------------------- 1 | 2 | let g:cm#snippet#snippets = [] 3 | 4 | func! cm#snippet#init() 5 | if ((!exists('g:cm_completed_snippet_enable') || g:cm_completed_snippet_enable) && !exists('g:cm_completed_snippet_engine')) 6 | if exists('g:loaded_neosnippet') 7 | let g:cm_completed_snippet_enable = 1 8 | let g:cm_completed_snippet_engine = 'neosnippet' 9 | elseif exists('g:did_plugin_ultisnips') 10 | let g:cm_completed_snippet_enable = 1 11 | let g:cm_completed_snippet_engine = 'ultisnips' 12 | elseif exists('g:snipMateSources') 13 | let g:cm_completed_snippet_enable = 1 14 | let g:cm_completed_snippet_engine = 'snipmate' 15 | else 16 | let g:cm_completed_snippet_enable = 0 17 | let g:cm_completed_snippet_engine = '' 18 | endif 19 | endif 20 | 21 | let g:cm_completed_snippet_enable = get(g:, 'cm_completed_snippet_enable', 0) 22 | let g:cm_completed_snippet_engine = get(g:, 'cm_completed_snippet_engine', '') 23 | 24 | if g:cm_completed_snippet_engine == 'neosnippet' 25 | call s:neosnippet_init() 26 | endif 27 | if g:cm_completed_snippet_engine == 'snipmate' 28 | call s:snipmate_init() 29 | endif 30 | endfunc 31 | 32 | func! cm#snippet#completed_is_snippet() 33 | call cm#snippet#check_and_inject() 34 | return get(v:completed_item, 'is_snippet', 0) 35 | endfunc 36 | 37 | func! cm#snippet#check_and_inject() 38 | 39 | if empty(v:completed_item) || !has_key(v:completed_item,'info') || empty(v:completed_item.info) || has_key(v:completed_item, 'is_snippet') 40 | return '' 41 | endif 42 | 43 | let l:last_line = split(v:completed_item.info,'\n')[-1] 44 | if l:last_line[0:len('snippet@')-1]!='snippet@' 45 | let v:completed_item.is_snippet = 0 46 | return '' 47 | endif 48 | 49 | let l:snippet_id = str2nr(l:last_line[len('snippet@'):]) 50 | if l:snippet_id>=len(g:cm#snippet#snippets) || l:snippet_id<0 51 | let v:completed_item.is_snippet = 0 52 | return '' 53 | endif 54 | 55 | " neosnippet recognize the snippet field of v:completed_item. Also useful 56 | " for checking. Kind of a hack. 57 | " TODO: skip empty g:cm#snippet#snippets[l:snippet_id]['snippet'] 58 | let v:completed_item.snippet = g:cm#snippet#snippets[l:snippet_id]['snippet'] 59 | let v:completed_item.snippet_word = g:cm#snippet#snippets[l:snippet_id]['word'] 60 | let v:completed_item.is_snippet = 1 61 | 62 | if v:completed_item.snippet == '' 63 | return '' 64 | endif 65 | 66 | if g:cm_completed_snippet_engine == 'ultisnips' 67 | 68 | call s:ultisnips_inject() 69 | 70 | " elseif g:cm_completed_snippet_engine == 'snipmate' 71 | " nothing needs to be done for snipmate 72 | 73 | elseif g:cm_completed_snippet_engine == 'neosnippet' 74 | 75 | call s:neosnippet_inject() 76 | 77 | endif 78 | 79 | return '' 80 | endfunc 81 | 82 | func! s:ultisnips_inject() 83 | if get(b:,'_cm_us_setup',0)==0 84 | " UltiSnips_Manager.add_buffer_filetypes('%s.snips.ncm' % vim.eval('&filetype')) 85 | let b:_cm_us_setup = 1 86 | let b:_cm_us_filetype = 'ncm' 87 | call UltiSnips#AddFiletypes(b:_cm_us_filetype) 88 | augroup cm 89 | autocmd InsertLeave exec g:_uspy 'UltiSnips_Manager._added_snippets_source._snippets["ncm"]._snippets = []' 90 | augroup END 91 | endif 92 | exec g:_uspy 'UltiSnips_Manager._added_snippets_source._snippets["ncm"]._snippets = []' 93 | call UltiSnips#AddSnippetWithPriority(v:completed_item.snippet_word, v:completed_item.snippet, '', 'i', b:_cm_us_filetype, 1) 94 | endfunc 95 | 96 | func! s:neosnippet_init() 97 | " Not compatible with neosnippet#enable_completed_snippet. NCM 98 | " choose a different approach 99 | let g:neosnippet#enable_completed_snippet=0 100 | augroup cm 101 | autocmd InsertEnter * call s:neosnippet_cleanup() 102 | augroup END 103 | let s:neosnippet_injected = [] 104 | endfunc 105 | 106 | func! s:neosnippet_inject() 107 | let snippets = neosnippet#variables#current_neosnippet() 108 | 109 | let item = {} 110 | let item['options'] = { "word": 1, "oneshot": 0, "indent": 0, "head": 0} 111 | let item['word'] = v:completed_item.snippet_word 112 | let item['snip'] = v:completed_item.snippet 113 | let item['description'] = '' 114 | 115 | let snippets.snippets[v:completed_item.snippet_word] = item 116 | 117 | " remember for cleanup 118 | let s:neosnippet_injected = add(s:neosnippet_injected, v:completed_item.snippet_word) 119 | endfunc 120 | 121 | func! s:neosnippet_cleanup() 122 | let cs = neosnippet#variables#current_neosnippet() 123 | for word in s:neosnippet_injected 124 | unlet cs.snippets[word] 125 | endfor 126 | let s:neosnippet_injected = [] 127 | endfunc 128 | 129 | func! s:snipmate_init() 130 | " inject ncm's handler into snipmate 131 | let g:snipMateSources.ncm = funcref#Function('cm#snippet#_snipmate_snippets') 132 | endfunc 133 | 134 | func! cm#snippet#_snipmate_snippets(scopes, trigger, result) 135 | if empty(v:completed_item) || get(v:completed_item, 'snippet', '') == '' 136 | return 137 | endif 138 | " use version 1 snippet syntax 139 | let a:result[v:completed_item.snippet_word] = {'default': [v:completed_item.snippet, 1] } 140 | endfunc 141 | 142 | -------------------------------------------------------------------------------- /autoload/cm/sources/css.vim: -------------------------------------------------------------------------------- 1 | 2 | func! cm#sources#css#init() 3 | call cm#register_source({'name' : 'cm-css', 4 | \ 'priority': 9, 5 | \ 'scoping': 1, 6 | \ 'scopes': ['css','scss', 'stylus', 'less', 'sass'], 7 | \ 'abbreviation': 'css', 8 | \ 'word_pattern': '[\w\-]+', 9 | \ 'cm_refresh_patterns':['[\w\-]+\s*:\s+'], 10 | \ 'cm_refresh': {'omnifunc': 'csscomplete#CompleteCSS'}, 11 | \ }) 12 | endfunc 13 | 14 | -------------------------------------------------------------------------------- /autoload/cm/sources/neosnippet.vim: -------------------------------------------------------------------------------- 1 | 2 | func! cm#sources#neosnippet#init() 3 | call cm#register_source({'name' : 'cm-neosnippet', 4 | \ 'priority': 7, 5 | \ 'abbreviation': 'Snip', 6 | \ 'word_pattern': '\S+', 7 | \ 'cm_refresh': 'cm#sources#neosnippet#cm_refresh', 8 | \ }) 9 | endfunc 10 | 11 | function! cm#sources#neosnippet#cm_refresh(info, ctx) 12 | let l:snips = values(neosnippet#helpers#get_completion_snippets()) 13 | let l:matches = map(l:snips, '{"word":v:val["word"], "dup":1, "icase":1, "menu": "Snip: " . v:val["menu_abbr"], "is_snippet": 1}') 14 | call cm#complete(a:info, a:ctx, a:ctx['startcol'], l:matches) 15 | endfunction 16 | 17 | " inoremap =cm#sources#neosnippet#trigger_or_popup("\(neosnippet_expand_or_jump)") 18 | func! cm#sources#neosnippet#trigger_or_popup(trigger_key) 19 | let l:ctx = cm#context() 20 | 21 | let l:typed = l:ctx['typed'] 22 | let l:kw = matchstr(l:typed,'\v\S+$') 23 | if len(l:kw) 24 | call feedkeys(a:trigger_key) 25 | return '' 26 | endif 27 | 28 | " notify the completion framework 29 | let l:ctx['startcol'] = 1 30 | call cm#sources#neosnippet#cm_refresh('cm-neosnippet', l:ctx) 31 | return '' 32 | endfunc 33 | 34 | -------------------------------------------------------------------------------- /autoload/cm/sources/snipmate.vim: -------------------------------------------------------------------------------- 1 | 2 | function! cm#sources#snipmate#init() 3 | call cm#register_source({'name' : 'cm-snipmate', 4 | \ 'priority': 7, 5 | \ 'abbreviation': 'Snip', 6 | \ 'word_pattern': '\S+', 7 | \ 'cm_refresh': 'cm#sources#snipmate#cm_refresh', 8 | \ }) 9 | endfunction 10 | 11 | function! cm#sources#snipmate#cm_refresh(info, ctx) 12 | let l:word = snipMate#WordBelowCursor() 13 | let l:matches = map(snipMate#GetSnippetsForWordBelowCursorForComplete(''),'extend(v:val,{"dup":1, "is_snippet":1})') 14 | call cm#complete(a:info, a:ctx, a:ctx['col']-len(l:word), l:matches) 15 | endfunction 16 | 17 | " inoremap =cm#sources#snipmate#trigger_or_popup("\snipMateTrigger") 18 | func! cm#sources#snipmate#trigger_or_popup(trigger_key) 19 | let l:word = snipMate#WordBelowCursor() 20 | if len(l:word) 21 | call feedkeys(a:trigger_key) 22 | return '' 23 | endif 24 | 25 | let l:ctx = cm#context() 26 | call cm#sources#snipmate#cm_refresh('cm-snipmate', l:ctx) 27 | return '' 28 | endfunc 29 | 30 | -------------------------------------------------------------------------------- /autoload/cm/sources/ultisnips.vim: -------------------------------------------------------------------------------- 1 | 2 | func! cm#sources#ultisnips#init() 3 | call cm#register_source({'name' : 'cm-ultisnips', 4 | \ 'priority': 7, 5 | \ 'abbreviation': 'Snip', 6 | \ 'word_pattern': '\S+', 7 | \ 'cm_refresh': 'cm#sources#ultisnips#cm_refresh', 8 | \ }) 9 | endfunc 10 | 11 | func! cm#sources#ultisnips#cm_refresh(opt, ctx) 12 | 13 | let l:snips = UltiSnips#SnippetsInCurrentScope() 14 | 15 | let l:matches = map(keys(l:snips),'{"word":v:val, "dup":1, "icase":1, "info": l:snips[v:val], "is_snippet": 1}') 16 | 17 | call cm#complete(a:opt, a:ctx, a:ctx['startcol'], l:matches) 18 | 19 | endfunc 20 | 21 | 22 | " Tips: Add this to your vimrc for triggering snips popup with 23 | " 24 | " let g:UltiSnipsExpandTrigger = "(ultisnips_expand)" 25 | " inoremap =cm#sources#ultisnips#trigger_or_popup("\(ultisnips_expand)") 26 | " 27 | func! cm#sources#ultisnips#trigger_or_popup(trigger_key) 28 | 29 | let l:ctx = cm#context() 30 | 31 | let l:typed = l:ctx['typed'] 32 | let l:kw = matchstr(l:typed,'\v\S+$') 33 | if len(l:kw) 34 | call feedkeys(a:trigger_key) 35 | return '' 36 | endif 37 | 38 | let l:ctx['startcol'] = 1 39 | call cm#sources#ultisnips#cm_refresh('cm-ultisnips', l:ctx) 40 | return '' 41 | endfunc 42 | 43 | -------------------------------------------------------------------------------- /doc/nvim-completion-manager.txt: -------------------------------------------------------------------------------- 1 | *nvim-completion-manager.txt* Fast, Extensible, Async Completion Framework 2 | 3 | Fast, Extensible, Async Completion Framework For Neovim~ 4 | 5 | Author: roxma 6 | License: MIT 7 | 8 | nvim-completion-manager *nvim-completion-manager* *NCM* 9 | 10 | 1. Introduction |NCM-introduction| 11 | 2. Install |NCM-install| 12 | 3. Optional Configuration Tips |NCM-tips| 13 | 4. Settings |NCM-settings| 14 | 5. API |NCM-API| 15 | 6. Why |NCM-why| 16 | 7. Minimal Source Examples |NCM-source-examples| 17 | 8. Trouble shooting |NCM-trouble-shooting| 18 | 9. Known issues |NCM-known-issues| 19 | 20 | ============================================================================== 21 | 1. Introduction *NCM-introduction* 22 | 23 | |NCM| (short for nvim-completion-manager) is a fast, extensible, async 24 | completion framework for neovim. 25 | 26 | Main features: 27 | 28 | 1. Asynchronous completion support like |deoplete|. 29 | 2. Faster, all completions should run in parallel. 30 | 3. Smarter on files with different languages, for example, css/javascript 31 | completion in html style/script tag. 32 | 4. Extensible async vimscript API and python3 API. 33 | 5. Function parameter expansion via |Ultisnips|, |neosnippet| or |SnipMate|. 34 | 35 | 36 | ============================================================================== 37 | 2. Install *NCM-install* 38 | 39 | Requirements: 40 | 41 | - For Neovim: 42 | - `has("python3")` 43 | - For Vim 8: 44 | - [roxma/vim-hug-neovim-rpc](https://github.com/roxma/vim-hug-neovim-rpc) 45 | - `g:python3_host_prog` pointed to your python3 executable, or `echo 46 | exepath('python3')` is not empty. 47 | - [neovim python client](https://github.com/neovim/python-client) (`pip3 48 | install neovim`) 49 | 50 | Install the required pip modules for your python3: 51 | > 52 | # neovim module is required 53 | # jedi library for python completion 54 | # mistune for language specific completion on markdown 55 | # psutil and setproctitle are optional utilities 56 | pip3 install --user neovim jedi mistune psutil setproctitle 57 | < 58 | Note: If you're using vim8, you also need to install the `neovim` pip module 59 | for the python, depending which on of `has("python")` has `has("python3")` you 60 | are using. You also need to install |vim-hug-neovim-rpc| Plugin: 61 | https://github.com/roxma/vim-hug-neovim-rpc 62 | 63 | ============================================================================== 64 | 3. Optional Configuration Tips *NCM-tips* 65 | 66 | Supress the annoying completion messages: 67 | > 68 | " don't give |ins-completion-menu| messages. For example, 69 | " '-- XXX completion (YYY)', 'match 1 of 2', 'The only match', 70 | set shortmess+=c 71 | < 72 | When the key is pressed while the popup menu is visible, it only hides 73 | the menu. Use this mapping to hide the menu and also start a new line. 74 | > 75 | inoremap (pumvisible() ? "\\" : "\") 76 | < 77 | Here is an example for expanding snippet in the popup menu with key. 78 | Suppose you use the key for expanding snippet. 79 | > 80 | imap (pumvisible() ? "\\(expand_or_nl)" : "\") 81 | imap (expand_or_nl) (cm#completed_is_snippet() ? "\":"\") 82 | < 83 | When using CTRL-C to leave insert mode, it does not trigger the autocmd 84 | |InsertLeave|. You should use CTRL-[, or map the CTRL-C to . 85 | > 86 | inoremap 87 | < 88 | Use to select the popup menu: 89 | > 90 | inoremap pumvisible() ? "\" : "\" 91 | inoremap pumvisible() ? "\" : "\" 92 | < 93 | If 'omnifunc' is the only available option, you may register it as a source 94 | for NCM. 95 | > 96 | " css completion via `csscomplete#CompleteCSS` 97 | " The `'cm_refresh_patterns'` is PCRE. 98 | " Be careful with `'scoping': 1` here, not all sources, especially omnifunc, 99 | " can handle this feature properly. 100 | au User CmSetup call cm#register_source({'name' : 'cm-css', 101 | \ 'priority': 9, 102 | \ 'scoping': 1, 103 | \ 'scopes': ['css','scss'], 104 | \ 'abbreviation': 'css', 105 | \ 'word_pattern': '[\w\-]+', 106 | \ 'cm_refresh_patterns':['[\w\-]+\s*:\s+'], 107 | \ 'cm_refresh': {'omnifunc': 'csscomplete#CompleteCSS'}, 108 | \ }) 109 | < 110 | Note: 'omnifunc' is implemented in a synchronous style, and vim-vimscript is 111 | single threaded, it would potentially block the ui with the introduction of a 112 | heavy weight 'omnifunc', for example the builtin phpcomplete. If you get some 113 | time, please try implementing a source for NCM as a replacement for the old 114 | style 'omnifunc'. 115 | 116 | There's no guarantee that this plugin will be compatible with other completion 117 | plugin in the same buffer. Use `let g:cm_smart_enable=0` and call 118 | |cm#enable_for_buffer()| to use this plugin for specific buffer. 119 | 120 | This example shows how to disable NCM's builtin tag completion. It's also 121 | possible to use |g:cm_sources_override| to override other default options of a 122 | completion source. 123 | > 124 | let g:cm_sources_override = { 125 | \ 'cm-tags': {'enable':0} 126 | \ } 127 | < 128 | *NCM-parameter-expansion* 129 | *NCM-snippet* 130 | Config , and for parameter expansion and jumping around 131 | placeholders. 132 | 133 | *NCM-Ultisnips* 134 | If you have |Ultisnips|: 135 | > 136 | let g:UltiSnipsExpandTrigger = "(ultisnips_expand)" 137 | let g:UltiSnipsJumpForwardTrigger = "" 138 | let g:UltiSnipsJumpBackwardTrigger = "" 139 | let g:UltiSnipsRemoveSelectModeMappings = 0 140 | " optional 141 | inoremap =cm#sources#ultisnips#trigger_or_popup("\(ultisnips_expand)") 142 | < 143 | *NCM-SnipMate* 144 | Or if you have |SnipMate|: 145 | > 146 | let g:snips_no_mappings = 1 147 | " imap pumvisible() ? "\\snipMateTrigger" : "\snipMateTrigger" 148 | " wrap the mapping 149 | imap (snipmate_force_trigger) pumvisible() ? "\\snipMateTrigger" : "\snipMateTrigger" 150 | " show a list of snippets when no the user has typed nothing 151 | inoremap =cm#sources#snipmate#trigger_or_popup("\(snipmate_force_trigger)") 152 | vmap snipMateTrigger 153 | imap pumvisible() ? "\\snipMateNextOrTrigger" : "\snipMateNextOrTrigger" 154 | vmap snipMateNextOrTrigger 155 | imap pumvisible() ? "\\snipMateBack" : "\snipMateBack" 156 | vmap snipMateBack 157 | < 158 | *NCM-neosnippet* 159 | Or if you have |neosnippet|: 160 | > 161 | imap (neosnippet_expand_or_jump) 162 | vmap (neosnippet_expand_or_jump) 163 | inoremap =cm#sources#neosnippet#trigger_or_popup("\(neosnippet_expand_or_jump)") 164 | vmap (neosnippet_expand_target) 165 | " expand parameters 166 | let g:neosnippet#enable_completed_snippet=1 167 | < 168 | 169 | ============================================================================== 170 | 4. Settings *NCM-settings* 171 | 172 | *g:cm_smart_enable* 173 | g:cm_smart_enable 174 | Enable the |NCM| for buffers automatically. unless the 175 | buffer size reach the limit specified by 176 | |g:cm_buffer_size_limit|, or 'buftype' option is not 177 | empty, which means that it's not a normal buffer. 178 | Default: 1 179 | 180 | g:cm_buffer_size_limit 181 | Used together with |g:cm_smart_enable|. 182 | Default: 1000000 183 | 184 | *g:cm_sources_enable* 185 | g:cm_sources_enable 186 | Automatically enable all registered sources by 187 | default. Set it to 0 if you want to manually enable 188 | the registered sources you want by setting the 189 | |g:cm_sources_override|. 190 | Default: 1 191 | 192 | *g:cm_sources_override* 193 | g:cm_sources_override 194 | Override the options used to register the source. 195 | This example shows how to disable NCM's builtin tag 196 | completion: > 197 | let g:cm_sources_override = { 198 | \ 'cm-tags': {'enable':0} 199 | \ } 200 | < See |cm#register_source()| for more information. 201 | 202 | *g:cm_complete_start_delay* 203 | g:cm_complete_start_delay 204 | Wait for an interval, in milliseconds, before 205 | candidate calculation, to improve editor performance 206 | for fast typing. This is useful when dealing with 207 | slow, sync completion source. 208 | Default: 0 209 | 210 | *g:cm_complete_popup_delay* 211 | g:cm_complete_popup_delay 212 | After candidate calculation has started, wait for an 213 | interval, in milliseconds, before popping up. This 214 | would reduce the popup menu flickering when multiple 215 | sources are updating the popup menu in a short 216 | interval. Use an interval long enough for computer and 217 | short enough for human. 218 | Default: 50 219 | 220 | *g:cm_matcher* 221 | g:cm_matcher 222 | A |Dict| specifies the matcher for filtering and 223 | sorting the completion candidates. 224 | Default: 225 | `{'module': 'cm_matchers.prefix_matcher', 'case': 'smartcase'})` 226 | See |g:cm_matcher.module| and |g:cm_matcher.case|. 227 | 228 | *g:cm_matcher.module* 229 | g:cm_matcher.module 230 | The matcher module. Available options are: 231 | 232 | `"cm_matchers.prefix_matcher"` 233 | Prefix matching. When you type "ab", it only 234 | matches candidates start with with "ab". 235 | `"cm_matchers.substr_matcher"` 236 | Sub-string matching. When you type "round", it 237 | matchings "round", "around", "surroundings" 238 | and so on. 239 | `"cm_matchers.fuzzy_matcher"` 240 | Fuzzy matching. 241 | Note: Known issue #8 on github, there may be 242 | undesired cursor flickering with fuzzy 243 | matcher. 244 | `"cm_matchers.abbrev_matcher"` 245 | A more intuitive version of fuzzy matching. 246 | This module requires the ag binary from 247 | the_silver_searcher 248 | 249 | Note: If you use non-prefix matcher, and haven't set 250 | the |g:cm_completekeys| option, NCM will use 251 | `"(cm_completefunc)"` as the default value, 252 | which will change the behavior of the key. 253 | 254 | *g:cm_matcher.case* 255 | g:cm_matcher.case 256 | Case sensitivity. Available options: 257 | `"case"` 258 | case-sensitive. 259 | `"icase"` 260 | Ignore case. 261 | `"smartcase"` 262 | A lowercase typing will match lower and upper 263 | case candidates. But an uppercase typing will 264 | only match uppercase candidates. 265 | 266 | *g:cm_completed_snippet_enable* 267 | g:cm_completed_snippet_enable 268 | Snippet support for completed item (:help 269 | |v:completed_item|). This is useful for function 270 | parameter expension. 271 | 272 | Enable by default if any one of the conditions is 273 | satisfied: 274 | 275 | - You have installed neosnippet with 276 | |g:neosnippet#enable_completed_snippet| set to 1 277 | 278 | - You have installed ultisnips 279 | 280 | - You have installed snipMate 281 | 282 | *g:cm_completed_snippet_engine* 283 | g:cm_completed_snippet_engine 284 | Available options: 285 | - "ultisnips" by default if UltiSnips is installed 286 | - "snipmate" by default if snipMate is installed 287 | - "neosnippet" by default if neosnippet is installed 288 | 289 | *g:cm_completekeys* 290 | g:cm_completekeys 291 | Available options: 292 | `"\(cm_complete)"` 293 | This is the default. 294 | `"\(cm_completefunc)"` 295 | This is the default when you're not using a 296 | prefix matcher. This key uses the 297 | |i_CTRL-X_CTRL-U| key to trigger the popup 298 | menu, so which will change the behafior of 299 | CTRL-U key. To avoid this behavior, use 300 | `"\(cm_omnifunc)"`. 301 | `"\(cm_omnifunc)"` 302 | 303 | *g:cm_auto_popup* 304 | g:cm_auto_popup 305 | If set to 0, then you have to map the 306 | `(cm_force_refresh)` key, and use this key to 307 | trigger the popup menu. 308 | Default: 1 309 | 310 | *g:cm_refresh_length* 311 | g:cm_refresh_length 312 | The default value for |cm_refresh_length|. 313 | 314 | Default: `[[1,4],[7,3]]` 315 | Format: a |List| as [[ min priority, min len ],...], 316 | or single integer value. 317 | 318 | The default value means that sources with priority 319 | between 1 and 6 will have the value of 4, and sources 320 | with priority >= 7 will have the value of 3. 321 | 322 | Priority >= 7 is more semantic completion source, So 323 | it's better to use a smaller value to trigger the 324 | popup than others. 325 | 326 | By the way, There are users who tend to use longer 327 | typing to trigger the popup to keep them from 328 | distraction. In this case, I would recommend using the 329 | `(cm_force_refresh)` key to trigger popup 330 | manually with short typing. 331 | 332 | *g:cm_multi_threading* 333 | g:cm_multi_threading 334 | If this options is set to 0. NCM could run in 335 | multi-process mode, each source is a standalone 336 | process. This is useful for debugging. 337 | Default: 1 if environment variable 338 | `$NVIM_NCM_MULTI_THREAD` is not set. 339 | 340 | *g:cm_completeopt* 341 | g:cm_completeopt 342 | The value of 'completeopt' when NCM is active. 343 | Default: "menu,menuone,noinsert,noselect" 344 | 345 | ============================================================================== 346 | 5. API *NCM-API* 347 | 348 | *cm#register_source()* 349 | cm#register_source({source}) 350 | Register a completion source. {source} is a |Dict| that defines the 351 | source, it may contain these fields: 352 | 353 | name *NCM-name* 354 | Required, the unique name of the completion source. 355 | 356 | abbreviation *NCM-abbreviation* 357 | May be displayed in the popup item to indicate which 358 | completion source the item comes from. 359 | 360 | enable If it is zero, this source will not be used. 361 | Default: |g:cm_sources_enable| 362 | 363 | priority *NCM-priority* 364 | Required, the priority is used to sort between other 365 | sources. A higher value indicates that the completion 366 | item of this source sorts before sources with lower 367 | priority value. 368 | *NCM-priority-values* 369 | Recommended priority definitions: 370 | 2 keyword from the otherfiles, from user's 371 | openning browsers, etc. 372 | 4 keyword from openning buffers 373 | 5 keyword from current buffer 374 | 6 file path 375 | 7 snippet hint 376 | 8 language specific keyword, but not smart 377 | 9 smart programming language aware completion 378 | 379 | 380 | scopes *NCM-scopes* 381 | A |List| of scope. The source will be activated when 382 | the current editting scope (e.g. current 'filetype') 383 | is in the source's scopes list. 384 | If this field is not set, it means this is a general 385 | purpose completion source, like keyword from buffer, 386 | it will be activated for all buffers. 387 | See |NCM-scoping| for more information. 388 | 389 | word_pattern *NCM-word_pattern* 390 | The pattern used to calculate |NCM-startcol|. 391 | Default: see pythonx/cm_default.py 392 | 393 | *cm_refresh_patterns* 394 | cm_refresh_patterns *NCM-cm_refresh_patterns* 395 | An extra |List| of PCRE patterns (python regex), 396 | besides |cm_refresh_length|, for auto triggering 397 | popup menu. 398 | 399 | *cm_refresh* 400 | cm_refresh *NCM-cm_refresh* 401 | Required for |NCM-vimscript-source|. A handler for the 402 | |cm_refresh-notification|. It is normally a |String| 403 | storing the name of the handler function. 404 | 405 | This field should not be set for source implemented 406 | via |NCM-python-source|. 407 | 408 | If the handler is a legacy 'omnifunc', which confirms 409 | to the |completion-functions| protocol, use a |Dict| 410 | like this instead: 411 | 412 | `{'omnifunc': 'csscomplete#CompleteCSS'}` 413 | 414 | *NCM-cm_refresh_length* 415 | cm_refresh_length *cm_refresh_length* 416 | The minimum length of the matching word for auto 417 | triggering popup menu. 418 | 419 | If it contains a negative value, 420 | |cm_refresh-notification| will only get triggered by 421 | |cm_refresh_patterns|. 422 | 423 | Otherwise, if len(matching word) >= cm_refresh_length, 424 | a |cm_refresh-notification| will be triggered for the 425 | source. 426 | 427 | Default: |g:cm_refresh_length| 428 | 429 | early_cache *NCM-early_cache* 430 | Cache the completion candidates whenever a word 431 | pattern matches, the result will not be popped up 432 | until the word gets long enough 433 | (|cm_refresh_length|) or matches 434 | |cm_refresh_patterns|. It feels faster when candidates 435 | are already cached before the popup menu is displayed. 436 | 437 | Default: 0. 438 | 439 | auto_popup 440 | If 0, only the |(cm_force_refresh)| key can 441 | trigger the |cm_refresh-notification| for this source. 442 | Default: 1 443 | 444 | *NCM-source-options* 445 | options *NCM-options* 446 | This field is ignored by NCM framework, but it will be 447 | passed to the source handler when handling 448 | |cm_refresh-notification|, it is recommended to use 449 | this field to store source specific options. 450 | 451 | *cm#complete()* 452 | cm#complete({name}, {context}, {startcol}, {matches}[, {refresh}]) 453 | Call this function to trigger the popup menu whenever you have 454 | completions candidates available. Usually this function is called 455 | after a |cm_refresh-notification| is sent to a source. 456 | 457 | {name} is the |NCM-name| used to register the source. 458 | 459 | {context} is an arugment passed to the |NCM-cm_refresh| handler. It 460 | is used for synchronization, if the user types more characters before 461 | |cm#complete()| is called, NCM will know the {context} is outdated, 462 | and then ignore the {matches} from this call. 463 | 464 | See |cm#context()| for more information. 465 | 466 | {startcol} and {matches} are used the same way as vim's builtin 467 | |complete()| function. 468 | 469 | {refresh} is 0 by default. Normally |NCM| will cache the result until 470 | another word started. And if there's cached result of a source, the 471 | |cm_refresh-notification| will not be sent to the source. 472 | 473 | If {refresh} is 1, |NCM| will send the notification reguardless of the 474 | cache of this |cm#complete()| call. 475 | 476 | *NCM-CmSetup* *CmSetup* 477 | autocmd User CmSetup 478 | This autocmd will be triggered once |NCM| started. 479 | 480 | Registering a source via this autocmd will avoid error when |NCM| has 481 | not been installed yet. And it also avoid the loading of 482 | autoload/cm.vim at neovim startup, so that |NCM| won't affect neovim's 483 | startup time. 484 | 485 | *cm#disable_source()* 486 | cm#disable_source({name}) 487 | Disable the registered source. 488 | 489 | *cm#context()* 490 | cm#context() 491 | Get the current typing context. This function returns a |Dict| with 492 | the following fields: 493 | 494 | bufnr 495 | Same as `bufnr('%')`. 496 | lnum 497 | Same as `line('.')`. 498 | col 499 | Same as `col('.')`. 500 | filetype 501 | Same as 'filetype' of the buffer. 502 | typed *cm#context().typed* 503 | The typed text. For example: > 504 | ^foo bar|baz 505 | < 506 | Where `^` indicates the start of line, and `|` 507 | indicates cursor position. This field should be: > 508 | "foo bar" 509 | < 510 | Note: In python, the value of `len(context["typed"])` 511 | may be different from the value of `context["col"]-1`. 512 | As specified by to |col()| function, the column number 513 | is the by index of the column position. This only 514 | matters if you're dealing with non English Unicode 515 | characters. 516 | filepath 517 | The path of current editting file. Same as 518 | `expand('%:p')` 519 | 520 | Note: When the context is passed via |cm_refresh-notification|, it has 521 | some extra fields: 522 | 523 | scope 524 | Current editting scope. See |NCM-scoping| 525 | 526 | startcol *NCM-startcol* 527 | Calaulated by NCM for convenience to pass it as an 528 | argument to the |cm#complete()| function. 529 | 530 | NCM use |NCM-word_pattern| to get the last typed word, 531 | then set startcol to the beginning of the word. If the 532 | typing ends with non-word-sequence, startcol will be 533 | set to current col. 534 | 535 | Note: You're not required to use the startcol provided 536 | by NCM. 537 | 538 | base 539 | Set according to the startcol. The value is the same 540 | as the {base} passed to |complete-functions|. 541 | 542 | force 543 | Non-zero value if this notification is triggered by 544 | `(cm_force_refresh)`. 545 | 546 | early_cache 547 | Non-zero value if this notification is triggered by 548 | early caching, the popup menu will no be updated for 549 | early caching. 550 | 551 | Other undocumented fields are necessary for the framework, a 552 | completion source should not change these values. 553 | 554 | *cm#context_changed()* 555 | cm#context_changed({context}) 556 | Check if the {context} is outdated. 557 | 558 | *cm#enable_for_buffer()* 559 | cm#enable_for_buffer() 560 | Enable |NCM| for current buffer. 561 | 562 | *cm#disable_for_buffer()* 563 | cm#disable_for_buffer() 564 | Disable |NCM| for current buffer. 565 | 566 | cm#completed_is_snippet() 567 | Here is an example for expanding snippet in the popup menu with 568 | key. Suppose you use the key for expanding snippet. 569 | > 570 | imap (pumvisible() ? "\\(expand_or_nl)" : "\") 571 | imap (expand_or_nl) (cm#completed_is_snippet() ? "\":"\") 572 | < 573 | *(cm_force_refresh)* 574 | (cm_force_refresh) 575 | A key to trigger the popup manually. 576 | 577 | Disable |g:cm_auto_popup| to display popup only with 578 | this key. 579 | 580 | ============================================================================== 581 | 6. Why *NCM-why* 582 | 583 | This project was started just for fun, and it's working pleasingly for me now. 584 | However, it seems there's lots of differences between deoplete, YCM, and 585 | nvim-completion-manager, by implementation. 586 | 587 | I haven't read the source of YCM yet. So here I'm describing the basic 588 | implementation of NCM (short for nvim-completion-manager) and some of the 589 | differences between deoplete and this plugin. 590 | 591 | *NCM-async-architecture* 592 | *cm_refresh-notification* 593 | 6.1 Async architecture 594 | 595 | Each completion source should be a thread or a standalone process, the manager 596 | notifies the completion source for any text changing, even when popup menu is 597 | visible. The completion source notifies the manager if there's any complete 598 | matches available. After some basic priority sorting between completion 599 | sources, and some simple filtering, the completion popup menu will be 600 | triggered with the `complete()` function by the completion manager. 601 | 602 | If some of the completion source is calculating matches for a long long time, 603 | the popup menu will still be shown quickly if other completion sources work 604 | properly. And if the user hasn't changed anything, the popup menu will be 605 | updated after the slow completion source finishes the work. 606 | 607 | As the time as of this plugin being created, the completion sources of 608 | deoplete are gathered with `gather_candidates()` of the `Source` object, 609 | inside a for loop, in deoplete's process. A slow completion source may defer 610 | the display of popup menu. Of course it will not block the ui. 611 | 612 | The good news is that deoplete has supported async `gather_candidates` now. 613 | But still, NCM is potentially faster because all completion sources run in 614 | parallel. 615 | 616 | 617 | 6.2 Scoping *NCM-scoping* 618 | 619 | I write markdown files with code blocks quite often, so I've also implemented 620 | language specific completion for markdown file. This is a framework feature, 621 | which is called scoping. It should work for any markdown code block whose 622 | language completion source is avaible to NCM. I've also added support for 623 | javascript completion in script tag of html files, and css completion in style 624 | tag. 625 | 626 | The idea was originated in 627 | [vim-syntax-compl-pop](https://github.com/roxma/vim-syntax-compl-pop). Since 628 | it's pure vimscript implementation, and there are some limitations currently 629 | with neovim's syntax api. It's very likely that vim-syntax-compl-pop doesn't 630 | work, for example, javascript completion in markdown or html script tag. So I 631 | use custom parser in NCM to implement the scoping features. 632 | 633 | 6.3 Experimental hacking 634 | 635 | Note that there's some hacking done in NCM. It uses a per 30ms timer to detect 636 | changes even popup menu is visible, as well as using the `TextChangedI` event, 637 | which only triggers when no popup menu is visible. This is important for 638 | implementing the async architecture. I'm hoping one day neovim will offer 639 | better option rather than a timer or the limited `TextChangedI`. 640 | 641 | ============================================================================== 642 | 7. Minimal Source Examples *NCM-source-examples* 643 | 644 | NCM has both vimscript API and python API for writing completion source. 645 | 646 | If you just want to write a completion source, it's easier to use the python 647 | API. The completion source will run in NCM's process. 648 | 649 | Sometimes it's more memory efficient for a feature-rich plugin to keep the 650 | completion running in the same system process along with other features. In 651 | this case, you should call the vimscript API from your plugin process. 652 | 653 | 7.1 Implementation in pure vimscript *NCM-vimscript-source* 654 | 655 | This minimal example shows how to popup "foo_bar" and "foo_baz" when "/" is 656 | typed. 657 | 658 | For more complete example, you mignt need to read the source code of 659 | autoload/cm/sources/ultisnips.vim of this plugin. 660 | 661 | Note: The `au User CmSetup` sould be place into your vimrc or the 'rtp'/plugin 662 | directory in order to register the autocmd before |NCM| started. 663 | > 664 | au User CmSetup call cm#register_source({'name' : 'foo bar', 665 | \ 'abbreviation': 'foo', 666 | \ 'priority': 8, 667 | \ 'cm_refresh_patterns': ['/'], 668 | \ 'cm_refresh': 'g:Foo_auto_popup', 669 | \ }) 670 | 671 | func! g:Foo_auto_popup(opt,ctx) 672 | let l:matches = ['foo_bar','foo_baz'] 673 | call cm#complete(a:opt['name'], a:ctx, a:ctx['startcol'], l:matches) 674 | endfunc 675 | > 676 | There're two arguments pass to |cm_refresh| handler. 677 | 678 | The first {opt} is the {source} which is used for |cm#register_source()|. 679 | 680 | See |cm#context()| for more information on the second argument {ctx}. 681 | 682 | 7.2 Implementation via NCM python framework *NCM-python-source* 683 | 684 | This minimal example shows how to popup "foo_bar" and "foo_baz" when "/" is 685 | typed. 686 | 687 | For more complete example, you mignt need to read the source code of 688 | pythonx/cm_sources/cm_gocode.py of this plugin. 689 | 690 | Note: This file foo.py should be placed into 'rtp'/pythonx/cm_sources/ 691 | directory. 692 | > 693 | # -*- coding: utf-8 -*- 694 | 695 | from cm import register_source, Base, getLogger 696 | register_source(name='foo_bar', 697 | abbreviation='foo', 698 | cm_refresh_patterns=[r'/'], 699 | priority=8) 700 | 701 | import re 702 | 703 | logger = getLogger(__name__) 704 | 705 | class Source(Base): 706 | 707 | def cm_refresh(self, info, ctx): 708 | matches = ['foo_bar','foo_baz'] 709 | startcol = ctx['startcol'] 710 | self.complete(info, ctx, startcol, matches) 711 | < 712 | You could enable logging for debugging. Read |NCM-trouble-shooting| for 713 | more information. 714 | 715 | Here's explanation on how |cm_refresh| is triggered, with the typing of 716 | `foo_b/foo` for example. 717 | 718 | 1. `f|` 719 | 2. `fo|` 720 | 3. `foo|` 721 | |cm_refresh| is triggered, with |NCM-startcol|=1, since the default 722 | |cm_refresh_length|=3 is matched. Popup menu displays. 723 | 4. `foo_|` 724 | Popup menu displays, with the cached results 'foo_bar' and 'foo_baz'. 725 | 5. `foo_b|` 726 | As above 727 | 6. `foo_b/|` 728 | |cm_refresh| is triggered, with |NCM-startcol|=7, since the '/' of 729 | |cm_refresh_patterns| is matched. Popup menu displays. 730 | 7. `foo_b/f|` 731 | Popup menu displays, with the cached results 'foo_bar' and 'foo_baz'. 732 | 8. `foo_b/fo|` 733 | As above 734 | 9. `foo_b/foo|` 735 | As above 736 | 737 | 738 | ============================================================================== 739 | 8. Trouble shooting *NCM-trouble-shooting* 740 | 741 | If something is broken when you play with NCM. Add something like this to your 742 | vimrc to generate log files. 743 | > 744 | let $NVIM_PYTHON_LOG_FILE="/tmp/nvim_log" 745 | let $NVIM_NCM_LOG_LEVEL="DEBUG" 746 | let $NVIM_NCM_MULTI_THREAD=0 747 | < 748 | After you've reproduced the error, check the content of the log files 749 | generated by NCM in current directory, especially `nvim.log_py3_cm_core`. 750 | 751 | If no log file is generated, and you get the message "nvim-completion-manager 752 | core channel terminated", then you have to try starting NCM manually, and see 753 | what's happaning. Follow these steps: 754 | 755 | 1. If you are using vim-hug-neovim-rpc on vim8, use `:echo 756 | neovim_rpc#serveraddr()` to get the rpc server address. (eg. 757 | `127.0.0.1:37744`). 758 | 2. If you're on neovim, use `:echo v:servername` to get the rpc server 759 | address. (eg. `/tmp/nvim5UoeSg/0`) 760 | 3. Find the `pythonx/cm_start.py` in NCM's installation directory, then you 761 | can start it manually. 762 | > 763 | # python3 pythonx/cm_start.py core {addr} 764 | python3 pythonx/cm_start.py core /tmp/nvim5UoeSg/0 765 | < 766 | If NCM is working, but some completion source is broken. For example, python 767 | completion is not working, you need to check the contents of the file 768 | `nvim.log_py3_cm_sources.cm_jedi`. If this log file is not generated, the 769 | python completion source may have failed to start somehow. Follow these steps 770 | to start it manually: 771 | 772 | 1. Find the rpc server address as before. 773 | 2. Find the completion source name via `:echo g:_cm_sources` or simply `:echo 774 | keys(g:_cm_sources)`. For python completion, It is `cm-jedi`, then use 775 | `:echo g:_cm_sources['cm-jedi']['channel']['module']` to get the module 776 | name, which would be `cm_sources.cm_jedi`. 777 | 3. Use the source name and the module name to start the source manually, 778 | > 779 | # python3 pythonx/cm_start.py channel {source_name} {module_name} {addr} 780 | python3 pythonx/cm_start.py channel cm-jedi cm_sources.cm_jedi /tmp/nvim5UoeSg/0 781 | < 782 | 783 | ============================================================================== 784 | 9. Known issues *NCM-known-issues* 785 | 786 | 9.1 CTRL-L of |popupmenu-keys| is broken *NCM-issue#127* 787 | 788 | It seems this key is in-compatible with vim's |complete()| function. 789 | 790 | Try This option: 791 | let g:cm_completekeys = "\(cm_omnifunc)" 792 | or 793 | let g:cm_completekeys = "\(cm_completefunc)" 794 | 795 | But this attempt won't help you with non-prefix-matcher. 796 | 797 | 798 | ============================================================================== 799 | vim: tw=78:ts=8:softtabstop=8:sw=8:ft=help:norl:noexpandtab:fen:noet: 800 | -------------------------------------------------------------------------------- /plugin/cm.vim: -------------------------------------------------------------------------------- 1 | if get(s:,'init','0') 2 | finish 3 | endif 4 | let s:init = 1 5 | 6 | if !has('nvim') && v:version<800 7 | finish 8 | endif 9 | 10 | let g:cm_buffer_size_limit = get(g:, 'cm_buffer_size_limit', 1000000) 11 | 12 | " multithreadig, saves more memory, enabled by default 13 | if !exists('g:cm_multi_threading') 14 | if $NVIM_NCM_MULTI_THREAD == '' 15 | let g:cm_multi_threading = 1 16 | else 17 | let g:cm_multi_threading = $NVIM_NCM_MULTI_THREAD 18 | endif 19 | endif 20 | 21 | let g:cm_matcher = get(g:,'cm_matcher',{'module': 'cm_matchers.prefix_matcher', 'case': 'smartcase'}) 22 | 23 | if !exists('g:cm_completekeys') 24 | if g:cm_matcher['module'] == 'cm_matchers.prefix_matcher' 25 | " (cm_complete) has no flickering issue with prefix_matcher. But 26 | " it has terrible popup flickering issue with fuzzy_matcher. 27 | let g:cm_completekeys = "\(cm_complete)" 28 | else 29 | " (cm_completefunc) has no popup flickering with fuzzy matcher. 30 | " But it has cursor flickering issue 31 | let g:cm_completekeys = "\(cm_completefunc)" 32 | endif 33 | endif 34 | 35 | let g:cm_auto_popup = get(g:,'cm_auto_popup',1) 36 | 37 | let g:cm_complete_start_delay = get(g:,'cm_complete_start_delay', 0) 38 | 39 | let g:cm_complete_popup_delay = get(g:, 'cm_complete_popup_delay', get(g:, 'cm_complete_delay', 50)) 40 | 41 | let g:cm_sources_enable = get(g:,'cm_sources_enable',1) 42 | 43 | let g:cm_sources_override = get(g:,'cm_sources_override',{}) 44 | 45 | let g:cm_refresh_length = get(g:, 'cm_refresh_length', get(g:, 'cm_refresh_default_min_word_len', [[1, 4], [7, 3]])) 46 | 47 | let g:cm_completeopt=get(g:,'cm_completeopt','menu,menuone,noinsert,noselect') 48 | 49 | au User CmSetup if exists('g:did_plugin_ultisnips') | call cm#sources#ultisnips#init() | endif 50 | au User CmSetup if exists('g:loaded_neosnippet') | call cm#sources#neosnippet#init() | endif 51 | au User CmSetup if exists('g:snipMateSources') | call cm#sources#snipmate#init() | endif 52 | au User CmSetup call cm#sources#css#init() 53 | 54 | func! s:startup(...) 55 | if get(g:,'cm_smart_enable',1) 56 | call cm#_auto_enable_check() 57 | augroup cm_smart_enable 58 | au! 59 | au BufEnter * call cm#_auto_enable_check() 60 | au OptionSet buftype call cm#_auto_enable_check() 61 | augroup end 62 | endif 63 | endfunc 64 | 65 | call timer_start(get(g:, 'cm_startup_delay', 100), function('s:startup')) 66 | -------------------------------------------------------------------------------- /pythonx/cm.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import importlib 4 | import logging 5 | from neovim.api import Nvim 6 | from neovim import attach, setup_logging 7 | 8 | def getLogger(name): 9 | 10 | def get_loglevel(): 11 | # logging setup 12 | level = logging.INFO 13 | if 'NVIM_PYTHON_LOG_LEVEL' in os.environ: 14 | l = getattr(logging, 15 | os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(), 16 | level) 17 | if isinstance(l, int): 18 | level = l 19 | if 'NVIM_NCM_LOG_LEVEL' in os.environ: 20 | l = getattr(logging, 21 | os.environ['NVIM_NCM_LOG_LEVEL'].strip(), 22 | level) 23 | if isinstance(l, int): 24 | level = l 25 | return level 26 | 27 | logger = logging.getLogger(__name__) 28 | logger.setLevel(get_loglevel()) 29 | return logger 30 | 31 | logger = getLogger(__name__) 32 | 33 | # python="python2" is only used for sources that depends on python2 libraries, 34 | # don't use it if possible 35 | def register_source(name, abbreviation='', priority=5, enable=True, events=[], python='python3', multi_thread=None, **kwargs): 36 | # implementation is put inside cm_core 37 | # 38 | # cm_core use a trick to only register the source withou loading the entire 39 | # module 40 | return 41 | 42 | 43 | # Base class for cm_core, sources, and scopers 44 | class Base: 45 | 46 | def __init__(self,nvim): 47 | 48 | """ 49 | :type nvim: Nvim 50 | """ 51 | 52 | self.nvim = nvim 53 | self.logger = getLogger(self.__module__) 54 | self.snippet_engine = self.nvim.vars['cm_completed_snippet_engine'] 55 | 56 | # allow a source to preprocess inputs before committing to the manager 57 | @property 58 | def matcher(self): 59 | 60 | nvim = self.nvim 61 | 62 | if not hasattr(self,'_matcher'): 63 | 64 | # from cm.matchers.prifex_matcher import Matcher 65 | matcher_opt = nvim.eval('g:cm_matcher') 66 | m = importlib.import_module(matcher_opt['module']) 67 | 68 | chcmp_smartcase = lambda a,b: a==b if a.isupper() else a==b.lower() 69 | chcmp_case = lambda a,b: a==b 70 | chcmp_icase = lambda a,b: a.lower()==b.lower() 71 | 72 | case = matcher_opt.get('case','') 73 | if case not in ['case','icase','smartcase']: 74 | ignorecase,smartcase = nvim.eval('[&ignorecase,&smartcase]') 75 | if smartcase: 76 | chcmp = chcmp_smartcase 77 | elif ignorecase: 78 | chcmp = chcmp_icase 79 | else: 80 | chcmp = chcmp_case 81 | elif case=='case': 82 | chcmp = chcmp_case 83 | elif case=='icase': 84 | chcmp = chcmp_icase 85 | else: 86 | # smartcase 87 | chcmp = chcmp_smartcase 88 | 89 | # cache result 90 | self._matcher = m.Matcher(nvim,chcmp) 91 | 92 | return self._matcher 93 | 94 | def get_pos(self, lnum , col, src): 95 | """ 96 | convert vim's lnum, col into pos 97 | """ 98 | if type(src)==type(""): 99 | lines = src.split('\n') 100 | else: 101 | lines = src.split(b'\n') 102 | 103 | pos = 0 104 | for i in range(lnum-1): 105 | pos += len(lines[i])+1 106 | pos += col-1 107 | 108 | return pos 109 | 110 | # convert pos into vim's lnum, col 111 | def get_lnum_col(self, pos, src): 112 | """ 113 | convert pos into vim's lnum, col 114 | """ 115 | splited = src.split("\n") 116 | p = 0 117 | for idx,line in enumerate(splited): 118 | if p<=pos and p+len(line)>=pos: 119 | return (idx+1,pos-p+1) 120 | p += len(line)+1 121 | 122 | def get_src(self,ctx): 123 | 124 | """ 125 | Get the source code of current scope identified by the ctx object. 126 | """ 127 | 128 | nvim = self.nvim 129 | 130 | bufnr = ctx['bufnr'] 131 | changedtick = ctx['changedtick'] 132 | 133 | key = (bufnr,changedtick) 134 | if key != getattr(self,'_cache_key',None): 135 | lines = nvim.buffers[bufnr][:] 136 | lines.append('') # \n at the end of buffer 137 | self._cache_src = "\n".join(lines) 138 | self._cache_key = key 139 | 140 | scope_offset = ctx.get('scope_offset',0) 141 | scope_len = ctx.get('scope_len',len(self._cache_src)) 142 | 143 | return self._cache_src[scope_offset:scope_offset+scope_len] 144 | 145 | def message(self, msgtype, msg): 146 | self.nvim.call('cm#message', msgtype, msg) 147 | 148 | def complete(self, name, ctx, startcol, matches, refresh=False): 149 | if isinstance(name,dict): 150 | name = name['name'] 151 | self.nvim.call('cm#complete', name, ctx, startcol, matches, refresh, async=True) 152 | 153 | def snippet_placeholder(self, num, txt=''): 154 | # TODO: this version is so simple, but I haven't met those complicated 155 | # use case 156 | txt = txt.replace('$', r'\$') 157 | txt = txt.replace('{', r'\{') 158 | txt = txt.replace('}', r'\}') 159 | if txt == '': 160 | return '${%s}' % num 161 | return '${%s:%s}' % (num, txt) 162 | 163 | def setup_neovim(serveraddr): 164 | 165 | logger.info("connecting to neovim server: %s",serveraddr) 166 | # create another connection to avoid synchronization issue? 167 | if len(serveraddr.split(':'))==2: 168 | serveraddr,port = serveraddr.split(':') 169 | port = int(port) 170 | nvim = attach('tcp',address=serveraddr,port=port) 171 | else: 172 | nvim = attach('socket',path=serveraddr) 173 | 174 | sync_rtp(nvim) 175 | return nvim 176 | 177 | def sync_rtp(nvim): 178 | """ 179 | sync sys.path with vim's rtp option 180 | """ 181 | # setup pythonx 182 | pythonxs = nvim.eval(r'globpath(&rtp,"pythonx",1) . "\n" . globpath(&rtp,"rplugin/python3",1)') 183 | for path in pythonxs.split("\n"): 184 | if not path: 185 | continue 186 | if path not in sys.path: 187 | sys.path.append(path) 188 | return nvim 189 | 190 | def start_and_run_channel(channel_type, serveraddr, source_name, modulename): 191 | 192 | # connect neovim and setup python environment 193 | nvim = setup_neovim(serveraddr) 194 | 195 | if channel_type == 'core': 196 | 197 | import cm_core 198 | handler = cm_core.CoreHandler(nvim) 199 | logger.info('starting core, enter event loop') 200 | 201 | elif channel_type == 'channel': 202 | 203 | if sys.version_info.major==2: 204 | # python2 doesn't support namespace package 205 | # use load_source as a workaround 206 | import imp 207 | file = modulename.replace('.','/') 208 | exp = 'globpath(&rtp,"pythonx/%s.py",1)' % file 209 | path = nvim.eval(exp).strip() 210 | logger.info('<%s> python2 file path: %s, exp: %s', source_name, path, exp) 211 | m = imp.load_source(modulename,path) 212 | # the previous module load may be hacked before, by register_source 213 | if not hasattr(m, 'Source'): 214 | m = imp.reload(m) 215 | else: 216 | m = importlib.import_module(modulename) 217 | # the previous module load may be hacked before, by register_source 218 | if not hasattr(m, 'Source'): 219 | m = importlib.reload(m) 220 | 221 | handler = m.Source(nvim) 222 | nvim.call('cm#_channel_started',source_name, nvim.channel_id, async=True) 223 | logger.info('<%s> handler created, entering event loop', source_name) 224 | 225 | 226 | handler.cm_running_ = False 227 | handler.cm_msgs_ = [] 228 | 229 | def on_request(method, args): 230 | logger.error('method: %s not implemented, ignore this request', method) 231 | 232 | def on_notification(method, args): 233 | 234 | # A trick to get rid of the greenlet coroutine without using the 235 | # next_message API. 236 | handler.cm_msgs_.append( (method,args) ) 237 | if handler.cm_running_: 238 | logger.info("delay notification handling, method[%s]", method) 239 | return 240 | 241 | handler.cm_running_ = True 242 | 243 | while handler.cm_msgs_: 244 | 245 | method, args = handler.cm_msgs_.pop(0) 246 | 247 | try: 248 | logger.debug('%s method: %s, args: %s', channel_type, method, args) 249 | 250 | if channel_type=='channel' and method=='cm_refresh': 251 | ctx = args[1] 252 | # The refresh calculation may be heavy, and the notification queue 253 | # may have outdated refresh events, it would be meaningless to 254 | # process these event 255 | if nvim.call('cm#context_changed',ctx): 256 | logger.info('context_changed, ignoring context: %s', ctx) 257 | continue 258 | 259 | func = getattr(handler,method,None) 260 | if func is None: 261 | logger.info('method: %s not implemented, ignore this message', method) 262 | continue 263 | 264 | func(*args) 265 | logger.debug('%s method %s completed', channel_type, method) 266 | except Exception as ex: 267 | logger.exception("Failed processing method: %s, args: %s", method, args) 268 | 269 | handler.cm_running_ = False 270 | 271 | def on_setup(): 272 | on_notification('cm_setup',[]) 273 | 274 | try: 275 | logger.info("<%s> entering event loop", source_name) 276 | # Use next_message is simpler, as a handler doesn't need to deal with 277 | # concurrent issue, but it has serious issue, 278 | # https://github.com/roxma/nvim-completion-manager/issues/35#issuecomment-284049103 279 | nvim.run_loop(on_request, on_notification, on_setup) 280 | except Exception as ex: 281 | logger.exception("nvim.run_loop failed, %s", ex) 282 | finally: 283 | # use at_exit to ensure the calling of cm_shutdown 284 | func = getattr(handler,'cm_shutdown',None) 285 | if func: 286 | func() 287 | if channel_type=='core': 288 | exit(0) 289 | 290 | -------------------------------------------------------------------------------- /pythonx/cm_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging 4 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 5 | 6 | import sys 7 | import os 8 | import re 9 | import copy 10 | import importlib 11 | import cm 12 | import subprocess 13 | import time 14 | import cm_default 15 | import threading 16 | import json 17 | 18 | logger = cm.getLogger(__name__) 19 | 20 | # use a trick to only register the source withou loading the entire 21 | # module 22 | class CmSkipLoading(Exception): 23 | pass 24 | 25 | class CoreHandler(cm.Base): 26 | 27 | def __init__(self,nvim): 28 | 29 | super().__init__(nvim) 30 | 31 | # process control information on channels 32 | self._channel_processes = {} 33 | self._channel_threads = {} 34 | 35 | # { '{source_name}': {'startcol': , 'matches'} 36 | self._matches = {} 37 | self._sources = {} 38 | self._subscope_detectors = {} 39 | self._last_startcol = 0 40 | self._last_matches = [] 41 | # should be True for supporting display menu directly without cm_refresh 42 | self._has_popped_up = True 43 | self._complete_timer = None 44 | self._last_ctx = None 45 | 46 | self._loaded_modules = {} 47 | 48 | def cm_setup(self): 49 | 50 | # after all sources are registered, so that all channels will be 51 | # started the first time cm_start_channels is called 52 | self.nvim.call('cm#_core_channel_started', self.nvim.channel_id, async=True) 53 | 54 | # load configurations 55 | self._servername = self.nvim.vars['_cm_servername'] 56 | self._start_py = self.nvim.vars['_cm_start_py_path'] 57 | self._py3 = self.nvim.vars['_cm_py3'] 58 | self._py2 = self.nvim.eval("get(g:,'python_host_prog','python2')") 59 | self._complete_delay = self.nvim.vars['cm_complete_popup_delay'] 60 | self._completed_snippet_enable = self.nvim.vars['cm_completed_snippet_enable'] 61 | self._completed_snippet_engine = self.nvim.vars['cm_completed_snippet_engine'] 62 | self._multi_thread = int(self.nvim.vars['cm_multi_threading']) 63 | 64 | self.cm_detect_modules() 65 | 66 | def cm_detect_modules(self): 67 | 68 | cm.sync_rtp(self.nvim) 69 | 70 | self._detect_sources() 71 | 72 | self._load_scopers() 73 | 74 | def _load_scopers(self): 75 | 76 | scoper_paths = self.nvim.eval("globpath(&rtp,'pythonx/cm_scopers/*.py',1)").split("\n") 77 | # auto find scopers 78 | for path in scoper_paths: 79 | if not path: 80 | continue 81 | try: 82 | modulename = os.path.splitext(os.path.basename(path))[0] 83 | modulename = "cm_scopers.%s" % modulename 84 | if modulename in self._loaded_modules: 85 | continue 86 | 87 | self._loaded_modules[modulename] = True 88 | 89 | m = importlib.import_module(modulename) 90 | 91 | scoper = m.Scoper(self.nvim) 92 | for scope in scoper.scopes: 93 | if scope not in self._subscope_detectors: 94 | self._subscope_detectors[scope] = [] 95 | self._subscope_detectors[scope].append(scoper) 96 | logger.info('scoper <%s> imported for %s', modulename, scope) 97 | 98 | except Exception as ex: 99 | logger.exception('importing scoper <%s> failed: %s', modulename, ex) 100 | 101 | logger.info('_subscope_detectors: %s', self._subscope_detectors) 102 | 103 | def _detect_sources(self): 104 | 105 | # auto find sources 106 | sources_paths = self.nvim.eval("globpath(&rtp,'pythonx/cm_sources/*.py',1)").split("\n") 107 | for path in sources_paths: 108 | 109 | modulename = os.path.splitext(os.path.basename(path))[0] 110 | modulename = "cm_sources.%s" % modulename 111 | 112 | if modulename in self._loaded_modules: 113 | continue 114 | 115 | # use a trick to only register the source withou loading the entire 116 | # module 117 | def register_source(name,abbreviation,priority,enable=True,events=[],python='python3',multi_thread=None,**kwargs): 118 | 119 | channel = dict(type=python, 120 | module=modulename, 121 | events=events) 122 | 123 | if not multi_thread is None: 124 | channel['multi_thread'] = multi_thread 125 | 126 | source = {} 127 | source['channel'] = channel 128 | source['name'] = name 129 | source['priority'] = priority 130 | source['enable'] = enable 131 | source['abbreviation'] = abbreviation 132 | for k in kwargs: 133 | source[k] = kwargs[k] 134 | 135 | logger.info('registering source: %s',source) 136 | self.nvim.call('cm#register_source', source, async=True) 137 | 138 | # use a trick to only register the source withou loading the entire 139 | # module 140 | raise CmSkipLoading() 141 | 142 | old_handler = cm.register_source 143 | cm.register_source = register_source 144 | try: 145 | # register_source 146 | m = importlib.import_module(modulename) 147 | except CmSkipLoading: 148 | # This is not an error 149 | logger.info('source <%s> registered', modulename) 150 | except Exception as ex: 151 | logger.exception("register_source for %s failed", modulename) 152 | finally: 153 | # restore 154 | cm.register_source = old_handler 155 | 156 | def _is_kw_futher_typing(self,info,oldctx,curctx): 157 | 158 | old_typed = oldctx['typed'] 159 | cur_typed = curctx['typed'] 160 | 161 | old_len = len(old_typed) 162 | cur_len = len(cur_typed) 163 | 164 | if cur_len < old_len: 165 | return False 166 | 167 | tmp_ctx1 = copy.deepcopy(oldctx) 168 | tmp_ctx2 = copy.deepcopy(curctx) 169 | 170 | if not self._check_refresh_patterns(info,tmp_ctx1,True): 171 | logger.debug('oldctx _check_refresh_patterns failed') 172 | return False 173 | if not self._check_refresh_patterns(info,tmp_ctx2,True): 174 | logger.debug('curctx _check_refresh_patterns failed') 175 | return False 176 | 177 | logger.debug('old ctx [%s] cur ctx [%s]', tmp_ctx1, tmp_ctx2) 178 | # startcol is set in self._check_refresh_patterns 179 | return tmp_ctx1['startcol'] == tmp_ctx2['startcol'] 180 | 181 | def cm_complete(self,srcs,name,ctx,startcol,matches,refresh,outdated,current_ctx): 182 | 183 | if isinstance(name,dict): 184 | name = name['name'] 185 | 186 | if name not in srcs: 187 | logger.error("invalid completion source name [%s]", name) 188 | return 189 | 190 | info = srcs[name] 191 | 192 | # be careful when completion matches context is outdated 193 | if outdated: 194 | logger.info("[%s] outdated matches, old typed [%s] cur typed[%s]", name, ctx['typed'], current_ctx['typed']) 195 | if refresh: 196 | logger.info("[%s] ignore outdated matching refresh=1", name) 197 | return 198 | if not self._is_kw_futher_typing(info,ctx,current_ctx): 199 | logger.info("[%s] matches is outdated. ignore them.", name) 200 | return 201 | logger.info("[%s] matches is outdated by keyword further typing. I'm gonna keep it.", name) 202 | 203 | # adjust for subscope 204 | if ctx['lnum']==1: 205 | startcol += ctx.get('scope_col',1)-1 206 | 207 | self._sources = srcs 208 | 209 | try: 210 | 211 | # process the matches early to eliminate unnecessary complete function call 212 | result = self.process_matches(name,ctx,startcol,matches) 213 | logger.debug('<%s> preprocessing result startcol: %s matches: %s', name, startcol, result) 214 | 215 | if (not result) and (not self._matches.get(name,{}).get('last_matches',[])): 216 | # not popping up, ignore this request 217 | logger.debug('Not popping up, not refreshing for cm_complete by %s, startcol %s', name, startcol) 218 | return 219 | 220 | finally: 221 | 222 | # storing matches 223 | 224 | if name not in self._matches: 225 | self._matches[name] = {} 226 | 227 | if len(matches)==0: 228 | del self._matches[name] 229 | else: 230 | complete_info = self._matches[name] 231 | complete_info['startcol'] = startcol 232 | complete_info['refresh'] = refresh 233 | complete_info['matches'] = matches 234 | complete_info['context'] = ctx 235 | if outdated and complete_info.get('enable',False): 236 | # outdated, but it is keyword further typing, do not 237 | # override the already enabled matches 238 | complete_info['enable'] = True 239 | else: 240 | complete_info['enable'] = not ctx.get('early_cache',False) 241 | 242 | # wait for _complete_timeout, reduce flashes 243 | if self._has_popped_up: 244 | logger.info("update popup for [%s]",name) 245 | # the ctx in parameter maybe a subctx for completion source, use 246 | # nvim.call to get the root context 247 | self._refresh_completions(self.nvim.call('cm#context')) 248 | else: 249 | logger.debug("delay popup for [%s]",name) 250 | 251 | def cm_insert_enter(self): 252 | self._matches = {} 253 | self._last_matches = [] 254 | self._last_startcol = 0 255 | 256 | # complete timer 257 | self._last_ctx = None 258 | if self._complete_timer: 259 | self._complete_timer.cancel() 260 | self._complete_timer = None 261 | 262 | def _on_complete_timeout(self,srcs,ctx,*args): 263 | if self._last_ctx!=ctx: 264 | logger.warn("_on_complete_timeout triggered, but last_ctx is %s, param ctx is %s", self._last_ctx, ctx) 265 | return 266 | if not self._has_popped_up: 267 | self._refresh_completions(ctx) 268 | self._has_popped_up = True 269 | else: 270 | logger.debug("ignore _on_complete_timeout for self._has_popped_up") 271 | 272 | def cm_refresh(self,srcs,root_ctx,force=0,*args): 273 | 274 | root_ctx['scope'] = root_ctx['filetype'] 275 | root_ctx['force'] = force 276 | 277 | # complete delay timer 278 | self._last_ctx = root_ctx 279 | self._has_popped_up = False 280 | if self._complete_timer: 281 | self._complete_timer.cancel() 282 | self._complete_timer = None 283 | 284 | # Note: get_src function asks neovim for data, during which another 285 | # greenlet coroutine could start or run, calculate this as soon as 286 | # possible to avoid concurrent issue 287 | ctx_list = self._get_ctx_list(root_ctx) 288 | 289 | self._sources = srcs 290 | 291 | if force: 292 | # if this is forcing refresh, clear the cached variable to avoid 293 | # being filtered by the self._complete function 294 | self._last_matches = [] 295 | self._last_startcol = 0 296 | 297 | # simple complete done 298 | if root_ctx['typed'] == '': 299 | self._matches = {} 300 | elif re.match(r'\s',root_ctx['typed'][-1]): 301 | self._matches = {} 302 | 303 | # do notify_sources_to_refresh 304 | refreshes_calls = [] 305 | refreshes_channels = [] 306 | 307 | # get the sources that need to be notified 308 | for ctx_item in ctx_list: 309 | for name in srcs: 310 | ctx = copy.deepcopy(ctx_item) 311 | ctx['early_cache'] = False 312 | ctx['force'] = force 313 | 314 | info = srcs[name] 315 | if not info['enable']: 316 | # ignore disabled source 317 | continue 318 | 319 | try: 320 | 321 | if not self._check_scope(ctx,info): 322 | logger.debug('_check_scope ignore <%s> for context scope <%s>', name, ctx['scope']) 323 | continue 324 | 325 | if not force and not info['auto_popup']: 326 | logger.debug('<%s> is not auto_popup', name) 327 | continue 328 | 329 | # refresh patterns 330 | if not self._check_refresh_patterns(info,ctx,force): 331 | if not force and info['early_cache'] and self._check_refresh_patterns(info,ctx,True): 332 | # early cache 333 | ctx['early_cache'] = True 334 | logger.debug('<%s> early_caching', name) 335 | else: 336 | logger.debug('cm_refresh ignore <%s>, force[%s] early_cache[%s]', name, force, info['early_cache']) 337 | if name in self._matches: 338 | self._matches[name]['enable'] = False 339 | continue 340 | else: 341 | # enable cached 342 | if name in self._matches: 343 | self._matches[name]['enable'] = True 344 | 345 | if ( 346 | (name in self._matches) and 347 | not self._matches[name]['refresh'] and 348 | not force and 349 | self._matches[name]['startcol']==ctx['startcol'] and 350 | ctx.get('match_end', '') == self._matches[name]['context'].get('match_end', '') 351 | ): 352 | logger.debug('<%s> has been cached, <%s> candidates', name, len(self._matches[name]['matches'])) 353 | continue 354 | 355 | if 'cm_refresh' in info: 356 | refreshes_calls.append(dict(name=name, context=ctx)) 357 | 358 | # start channels on demand here 359 | if 'channel' in info: 360 | channel = info['channel'] 361 | if 'id' not in channel: 362 | self._start_channel(info) 363 | 364 | channel = info.get('channel',{}) 365 | if 'id' in channel: 366 | refreshes_channels.append(dict(name=name, id=channel['id'], context=ctx)) 367 | except Exception as ex: 368 | logger.exception('cm_refresh exception: %s', ex) 369 | continue 370 | 371 | if not refreshes_calls and not refreshes_channels: 372 | logger.info('not notifying any channels, _refresh_completions now') 373 | self._refresh_completions(root_ctx) 374 | self._has_popped_up = True 375 | else: 376 | logger.info('notify_sources_to_refresh calls cnt [%s], channels cnt [%s]',len(refreshes_calls),len(refreshes_channels)) 377 | logger.debug('cm#_notify_sources_to_refresh [%s] [%s] [%s]', [e['name'] for e in refreshes_calls], [e['name'] for e in refreshes_channels], root_ctx) 378 | self.nvim.call('cm#_notify_sources_to_refresh', refreshes_calls, refreshes_channels, root_ctx, async=True) 379 | 380 | # complete delay timer 381 | def on_timeout(): 382 | self.nvim.async_call(self._on_complete_timeout, srcs, root_ctx) 383 | self._complete_timer = threading.Timer(float(self._complete_delay)/1000, on_timeout ) 384 | self._complete_timer.start() 385 | 386 | def _get_ctx_list(self,root_ctx): 387 | ctx_list = [root_ctx,] 388 | 389 | # scoping 390 | i = 0 391 | while i= len(typed)-word_len: 456 | ctx['match_end'] = matched.end() 457 | return True 458 | 459 | min_len = info['cm_refresh_length'] 460 | 461 | # always match 462 | if min_len==0: 463 | return True 464 | 465 | if force and word_len>0: 466 | return True 467 | 468 | if min_len > 0 and word_len >= min_len: 469 | return True 470 | 471 | return False 472 | 473 | # almost the same as `s:check_scope` in `autoload/cm.vim` 474 | def _check_scope(self,ctx,info): 475 | scopes = info.get('scopes',None) 476 | cur_scope = ctx.get('scope',ctx['filetype']) 477 | is_root_scope = ( cur_scope==ctx['filetype'] ) 478 | ctx['scope_match'] = '' 479 | if not scopes: 480 | # scopes setting is None, means that this is a general purpose 481 | # completion source, only complete for the root scope 482 | if is_root_scope: 483 | return True 484 | else: 485 | return False 486 | for scope in scopes: 487 | if scope==cur_scope: 488 | ctx['scope_match'] = scope 489 | if info.get('scoping',False): 490 | return True 491 | else: 492 | return is_root_scope 493 | return False 494 | 495 | def _refresh_completions(self,ctx): 496 | """ 497 | Note: This function is called via greenlet coroutine. Be careful, avoid 498 | using blocking requirest. 499 | """ 500 | 501 | matches = [] 502 | 503 | # sort by priority 504 | names = sorted(self._matches.keys(),key=lambda x: self._sources[x]['priority'], reverse=True) 505 | 506 | if len(names)==0: 507 | # empty 508 | logger.info('_refresh_completions names: %s, startcol: %s, matches: %s', names, ctx['col'], []) 509 | self._complete(ctx, ctx['col'], []) 510 | return 511 | 512 | col = ctx['col'] 513 | startcol = col 514 | 515 | # basick processing per source 516 | for name in names: 517 | 518 | try: 519 | 520 | self._matches[name]['last_matches'] = [] 521 | 522 | # may be disabled due to early_cache 523 | if not self._matches[name].get('enable',True): 524 | logger.debug('<%s> ignore by disabled', name) 525 | continue 526 | 527 | source_startcol = self._matches[name]['startcol'] 528 | if source_startcol>col or source_startcol==0: 529 | self._matches[name]['last_matches'] = [] 530 | logger.error('ignoring invalid startcol for %s %s', name, self._matches[name]['startcol']) 531 | continue 532 | 533 | source_matches = self._matches[name]['matches'] 534 | source_matches = self.process_matches(name,ctx,source_startcol,source_matches) 535 | 536 | self._matches[name]['last_matches'] = source_matches 537 | 538 | if not source_matches: 539 | continue 540 | 541 | # min non empty source_matches's source_startcol as startcol 542 | if source_startcol < startcol: 543 | startcol = source_startcol 544 | 545 | except Exception as inst: 546 | logger.exception('_refresh_completions process exception: %s', inst) 547 | continue 548 | 549 | # merge results of sources 550 | for name in names: 551 | 552 | try: 553 | source_startcol = self._matches[name]['startcol'] 554 | source_matches = self._matches[name]['last_matches'] 555 | if not source_matches: 556 | continue 557 | 558 | prefix = ctx['typed'][startcol-1 : source_startcol-1] 559 | 560 | for e in source_matches: 561 | # do the padding in vimscript to avoid the rpc 562 | # overhead of calling strdisplaywidth 563 | e['padding'] = prefix 564 | if 'abbr' not in e: 565 | e['abbr'] = e['word'] 566 | e['snippet_word'] = e['word'] 567 | e['word'] = prefix + e['word'] 568 | 569 | matches += source_matches 570 | 571 | except Exception as inst: 572 | logger.exception('_refresh_completions process exception: %s', inst) 573 | continue 574 | 575 | if not matches: 576 | startcol=len(ctx['typed']) or 1 577 | logger.info('_refresh_completions names: %s, startcol: %s, matches cnt: %s', names, startcol, len(matches)) 578 | logger.debug('_refresh_completions names: %s, startcol: %s, matches: %s, source matches: %s', names, startcol, matches, self._matches) 579 | self._complete(ctx, startcol, matches) 580 | 581 | def process_matches(self,name,ctx,startcol,matches): 582 | 583 | info = self._sources[name] 584 | abbr = info.get('abbreviation','') 585 | 586 | # formalize datastructure 587 | formalized = [] 588 | for item in matches: 589 | e = {} 590 | if type(item)==type(''): 591 | e['word'] = item 592 | else: 593 | e = copy.deepcopy(item) 594 | e['icase'] = 1 595 | formalized.append(e) 596 | 597 | # filtering and sorting 598 | result = self.matcher.process(info,ctx,startcol,formalized) 599 | 600 | # fix some text 601 | for e in result: 602 | 603 | if 'menu' not in e: 604 | if 'info' in e and e['info'] and len(e['info'])<50: 605 | if abbr: 606 | e['menu'] = "<%s> %s" % (abbr,e['info']) 607 | else: 608 | e['menu'] = e['info'] 609 | else: 610 | # info too long 611 | if abbr: 612 | e['menu'] = "<%s>" % abbr 613 | else: 614 | # e['menu'] = "<%s> %s" % (self._sources[name]['abbreviation'], e['info']) 615 | pass 616 | 617 | return result 618 | 619 | 620 | def _complete(self, ctx, startcol, matches): 621 | if not matches and not self._last_matches: 622 | # no need to fire complete message 623 | logger.info('matches==0, _last_matches==0, ignore') 624 | return 625 | not_changed = 0 626 | if self._last_startcol==startcol and self._last_matches==matches: 627 | not_changed = 1 628 | logger.info('ignore _complete call: self._last_startcol==startcol and self._last_matches==matches') 629 | 630 | # Note: The snippet field will not be kept in v:completed_item. Use 631 | # this trick to to hack 632 | snippets = [] 633 | has_snippets = False 634 | if self._completed_snippet_enable: 635 | for m in matches: 636 | 637 | if not m.get('snippet', None) and not m.get('is_snippet', None): 638 | continue 639 | 640 | has_snippets = True 641 | 642 | snippet = m.get('snippet', '') 643 | 644 | if 'info' not in m or not m['info']: 645 | m['info'] = 'snippet@%s' % len(snippets) 646 | else: 647 | m['info'] += '\nsnippet@%s' % len(snippets) 648 | 649 | # snippet word should not contain spaces 650 | rp = m['snippet_word'].split(' ')[0] 651 | m['word'] = m['word'][:-len(m['snippet_word'])] + rp 652 | m['snippet_word'] = rp 653 | 654 | snippets.append(dict(snippet=snippet, word=m['snippet_word'])) 655 | 656 | if has_snippets: 657 | for m in matches: 658 | if 'menu' not in m: 659 | m['menu'] = '' 660 | if m.get('snippet', None) or m.get('is_snippet', None): 661 | # [+] sign indicates that this completion item is 662 | # expandable 663 | m['menu'] = '[+] ' + m['menu'] 664 | else: 665 | m['menu'] = '[ ] ' + m['menu'] 666 | 667 | self.nvim.call('cm#_core_complete', ctx, startcol, matches, not_changed, snippets) 668 | self._last_matches = matches 669 | self._last_startcol = startcol 670 | 671 | 672 | def _start_channel(self,info): 673 | 674 | if 'channel' not in info: 675 | logger.error("this source does not use channel: %s", info) 676 | return 677 | 678 | name = info['name'] 679 | channel = info['channel'] 680 | channel_type = channel.get('type','') 681 | 682 | py = '' 683 | if channel_type=='python3': 684 | py = self._py3 685 | elif channel_type=='python2': 686 | py = self._py2 687 | else: 688 | logger.info("Unsupported channel_type [%s]",channel_type) 689 | 690 | if name not in self._channel_processes: 691 | self._channel_processes[name] = {} 692 | if name not in self._channel_threads: 693 | self._channel_threads[name] = {} 694 | 695 | process_info = self._channel_processes[name] 696 | thread_info = self._channel_threads[name] 697 | 698 | # channel process already started 699 | if 'proc' in process_info or 'thread' in thread_info: 700 | return 701 | 702 | if self._multi_thread and channel_type=='python3' and channel.get('multi_thread',1) and sys.version_info.major>=3: 703 | logger.info("starting <%s> thread channel", name) 704 | thread_info['thread'] = threading.Thread( 705 | target=cm.start_and_run_channel, 706 | name=name, 707 | args=('channel', self._servername, name, channel['module']) 708 | ) 709 | thread_info['thread'].start() 710 | return 711 | 712 | cmd = [py, self._start_py, 'channel', name, channel['module'], self._servername] 713 | 714 | # has not been started yet, start it now 715 | logger.info('starting channels for %s: %s',name, cmd) 716 | 717 | proc = subprocess.Popen(cmd,stdin=subprocess.DEVNULL,stdout=sys.stdout,stderr=sys.stderr) 718 | process_info['pid'] = proc.pid 719 | process_info['proc'] = proc 720 | 721 | logger.info('source <%s> channel pid: %s', name, proc.pid) 722 | 723 | def cm_shutdown(self): 724 | 725 | # wait for channel-threads' exit 726 | for name in self._channel_threads: 727 | tinfo = self._channel_threads[name] 728 | if 'thread' not in tinfo: 729 | continue 730 | try: 731 | logger.info("join <%s> thread", name) 732 | tinfo['thread'].join(2) 733 | logger.info("success join <%s> thread", name) 734 | except Exception as ex: 735 | logger.exception("timeout join <%s> thread", name) 736 | 737 | # wait for normal exit 738 | time.sleep(1) 739 | 740 | procs = [] 741 | for name in self._channel_processes: 742 | pinfo = self._channel_processes[name] 743 | if 'proc' not in pinfo: 744 | continue 745 | proc = pinfo['proc'] 746 | try: 747 | if proc.poll() is not None: 748 | logger.info("channel %s already terminated", name) 749 | continue 750 | procs.append((name,proc)) 751 | logger.info("terminating channel %s", name) 752 | proc.terminate() 753 | except Exception as ex: 754 | logger.exception("send terminate signal failed for %s", name) 755 | 756 | if not procs: 757 | return 758 | 759 | # wait for terminated 760 | time.sleep(1) 761 | 762 | # kill all 763 | for name,proc in procs: 764 | try: 765 | if proc.poll() is not None: 766 | logger.info("channel %s has terminated", name) 767 | continue 768 | logger.info("killing channel %s", name) 769 | proc.kill() 770 | logger.info("hannel %s killed", name) 771 | except Exception as ex: 772 | logger.exception("send kill signal failed for %s", name) 773 | 774 | def cm_start_channels(self,srcs,ctx): 775 | 776 | names = sorted(srcs.keys(),key=lambda n: srcs[n]['priority'], reverse=True) 777 | for name in names: 778 | 779 | info = srcs[name] 780 | 781 | if not info['enable']: 782 | continue 783 | 784 | # this source is not using channel 785 | if 'channel' not in info: 786 | continue 787 | 788 | # channel already started 789 | if info['channel'].get('id',None): 790 | continue 791 | 792 | if not self._check_scope(ctx,info): 793 | continue 794 | 795 | self._start_channel(info) 796 | 797 | -------------------------------------------------------------------------------- /pythonx/cm_default.py: -------------------------------------------------------------------------------- 1 | 2 | # sane default for programming languages 3 | 4 | 5 | _patterns = {} 6 | _patterns['*'] = r'(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)' 7 | _patterns['css'] = r'(-?\d*\.\d[\w-]*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)' 8 | _patterns['scss'] = _patterns['css'] 9 | _patterns['php'] = r'(-?\d*\.\d\w*)|([^\-\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)' 10 | _patterns['vim'] = r'(-?\d*\.\d\w*)|([^\-\`\~\!\@\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\'\"\,\.\<\>\/\?\s]+)' 11 | 12 | def word_pattern(ctx): 13 | scope = ctx.get('scope',ctx.get('filetype','')).lower() 14 | return _patterns.get(scope, None) or _patterns['*'] 15 | -------------------------------------------------------------------------------- /pythonx/cm_matchers/abbrev_matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import distutils.spawn 4 | import os 5 | import pipes 6 | import shlex 7 | import subprocess 8 | 9 | 10 | def _word_boundary(prev, curr): 11 | """Whether current character is on word boundary.""" 12 | if not prev: 13 | return True 14 | return ((curr.isupper() and not prev.isupper()) or 15 | (curr.islower() and not prev.isalpha()) or 16 | (curr.isdigit() and not prev.isdigit()) or 17 | (not curr.isalnum() and curr != prev)) 18 | 19 | 20 | def _match_generator(pattern, string, offset=0): 21 | """Recursively generate matches of `pattern` in `string`.""" 22 | 23 | def _find_ignorecase(string, char, start=0): 24 | """Find first occurrence of `char` inside `string`, 25 | starting with `start`-th character.""" 26 | if char.isalpha(): 27 | lo = string.find(char.lower(), start) 28 | hi = string.find(char.upper(), start) 29 | if lo == -1: 30 | return hi 31 | elif hi == -1: 32 | return lo 33 | else: 34 | return min(hi, lo) 35 | else: 36 | return string.find(char, start) 37 | 38 | if pattern == '': 39 | yield [] 40 | return 41 | 42 | if string == '': 43 | return 44 | 45 | indices = range(len(string)) 46 | 47 | abbrev_0 = pattern[0] 48 | abbrev_rest = pattern[1:] 49 | 50 | if abbrev_0.lower() == string[0].lower(): 51 | matches = _match_generator(abbrev_rest, string[1:], offset + 1) 52 | for m in matches: 53 | m.insert(0, offset) 54 | yield m 55 | 56 | i = _find_ignorecase(string, abbrev_0, 1) 57 | while i != -1: 58 | curr = string[i] 59 | 60 | prev = string[i - 1] 61 | if _word_boundary(prev, curr): 62 | matches = _match_generator(abbrev_rest, string[i + 1:], 63 | offset + i + 1) 64 | for m in matches: 65 | m.insert(0, offset + i) 66 | yield m 67 | 68 | i = _find_ignorecase(string, abbrev_0, i + 1) 69 | 70 | 71 | def make_regex(pattern, escape=False): 72 | """Build regular expression corresponding to `pattern`.""" 73 | 74 | def re_group(r): 75 | return r'(' + r + r')' 76 | 77 | def re_or(r1, r2): 78 | return re_group(re_group(r1) + '|' + re_group(r2)) 79 | 80 | def re_opt(r): 81 | return re_group(r) + '?' 82 | 83 | asterisk = '*' 84 | res = '' 85 | res += '^' 86 | for i, ch in enumerate(pattern): 87 | match_start = '' 88 | 89 | if ch.isalpha(): 90 | ch_lower = ch.lower() 91 | ch_upper = ch.upper() 92 | not_alpha = '[^a-zA-Z]' 93 | not_upper = '[^A-Z]' 94 | anycase = (re_opt(r'.{asterisk}{not_alpha}') + '{match_start}' + 95 | '[{ch_lower}{ch_upper}]') 96 | camelcase = re_opt(r'.{asterisk}{not_upper}') + '{ch_upper}' 97 | ch_res = re_or(anycase, camelcase) 98 | elif ch.isdigit(): 99 | ch_res = (re_opt(r'.{asterisk}[^0-9]') + '{match_start}{ch}') 100 | else: 101 | ch_res = r'.{asterisk}\{match_start}{ch}' 102 | res += ch_res.format(**locals()) 103 | if escape: 104 | res = res.replace('\\', '\\\\') 105 | return res 106 | 107 | 108 | def is_exe(path): 109 | return os.path.isfile(path) and os.access(path, os.X_OK) 110 | 111 | 112 | def which(exe): 113 | if not is_exe(exe): 114 | found = distutils.spawn.find_executable(exe) 115 | if found is not None and is_exe(found): 116 | return found 117 | return exe 118 | 119 | 120 | def filter_grep(regex, strings, cmd='ag --numbers'): 121 | """Return list of indexes in `strings` which match `regex`""" 122 | arg_list = shlex.split(cmd) 123 | arg_list[0] = which(arg_list[0]) 124 | arg_list.append(regex) 125 | cmd_str = ' '.join(pipes.quote(arg) for arg in arg_list) 126 | 127 | popen_kwargs = dict(creationflags=0x08000000) if os.name == 'nt' else {} 128 | try: 129 | grep = subprocess.Popen(arg_list, 130 | stdin=subprocess.PIPE, 131 | stdout=subprocess.PIPE, 132 | stderr=subprocess.PIPE, **popen_kwargs) 133 | except BaseException as exc: 134 | msg = 'Exception when executing "{}": {}'.format(cmd_str, exc) 135 | raise Exception(msg) 136 | out, err = grep.communicate(b'\n'.join([i.encode() for i in strings])) 137 | if err or grep.returncode == 2: 138 | msg = 'Command "{}" exited with return code {} and stderr "{}"'.format( 139 | cmd_str, grep.returncode, err.strip()) 140 | raise Exception(msg) 141 | res = [] 142 | for out_str in out.splitlines(): 143 | splitted = out_str.split(b':', 1) 144 | try: 145 | assert len(splitted) == 2 146 | line_num = int(splitted[0]) 147 | except: 148 | msg = 'Output "{}" does not contain line number (wrong grep arguments?)' 149 | raise Exception(msg.format(out_str)) 150 | res.append(line_num - 1) 151 | return res 152 | 153 | 154 | class Matcher(object): 155 | 156 | def __init__(self,nvim,chcmp,*args): 157 | self._chcmp = chcmp 158 | 159 | def process(self,info,ctx,startcol,matches): 160 | 161 | # generator to list 162 | matches = list(matches) 163 | 164 | # fix for chinese characters 165 | # `你好 abcd|` 166 | # has col('.')==11 on vim 167 | # the evaluated startcol is: startcol[8] typed[你好 abcd] 168 | # but in python, "你好 abcd"[8] is not a valid index 169 | begin = -(ctx['col'] - startcol) 170 | base = '' 171 | if begin: 172 | base = ctx['typed'][begin:] 173 | regex = make_regex(base) 174 | tmp = [item['word'] for item in matches] 175 | indices = filter_grep(regex, tmp) 176 | 177 | return [matches[i] for i in indices] 178 | 179 | -------------------------------------------------------------------------------- /pythonx/cm_matchers/fuzzy_matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Matcher(object): 5 | 6 | def __init__(self,nvim,chcmp,*args): 7 | self._chcmp = chcmp 8 | 9 | def process(self,info,ctx,startcol,matches): 10 | 11 | # fix for chinese characters 12 | # `你好 abcd|` 13 | # has col('.')==11 on vim 14 | # the evaluated startcol is: startcol[8] typed[你好 abcd] 15 | # but in python, "你好 abcd"[8] is not a valid index 16 | begin = -(ctx['col'] - startcol) 17 | base = '' 18 | if begin: 19 | base = ctx['typed'][begin:] 20 | 21 | 22 | tmp = [] 23 | for item in matches: 24 | score = self._match(base,item) 25 | if not score: 26 | continue 27 | tmp.append((item,score)) 28 | 29 | if info['sort']: 30 | # sort by score, the smaller the better 31 | tmp.sort(key=lambda e: e[1]) 32 | 33 | return [e[0] for e in tmp] 34 | 35 | # return the score, the smaller the better 36 | def _match(self, base, item): 37 | 38 | word = item['word'] 39 | if len(base)>len(word): 40 | return None 41 | 42 | min_range = None 43 | for i in range(len(word)-len(base)+1): 44 | r = self._get_match_range(base,word,i) 45 | if not r: 46 | break 47 | if not min_range: 48 | min_range = r 49 | if r[1]-r[0] < min_range[1]-min_range[0]: 50 | min_range = r 51 | 52 | if not min_range: 53 | return None 54 | 55 | # return the score, the smaller the better 56 | # prefer shorter match 57 | # prefer fronter match 58 | return (min_range[1]-min_range[0], min_range[0], word.swapcase()) 59 | 60 | def _get_match_range(self, base, word, start): 61 | p = start-1 62 | pend = len(word) 63 | begin = pend 64 | for c in base: 65 | p += 1 66 | if p>=pend: 67 | return None 68 | while not self._chcmp(c,word[p]): 69 | p += 1 70 | if p>=pend: 71 | return None 72 | if p < begin: 73 | begin = p 74 | return (begin,p) 75 | 76 | -------------------------------------------------------------------------------- /pythonx/cm_matchers/prefix_matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Matcher(object): 5 | 6 | def __init__(self,nvim,chcmp,*args): 7 | self._chcmp = chcmp 8 | 9 | def process(self,info,ctx,startcol,matches): 10 | 11 | # fix for chinese characters 12 | # `你好 abcd|` 13 | # has col('.')==11 on vim 14 | # the evaluated startcol is: startcol[8] typed[你好 abcd] 15 | # but in python, "你好 abcd"[8] is not a valid index 16 | begin = -(ctx['col'] - startcol) 17 | base = '' 18 | if begin: 19 | base = ctx['typed'][begin:] 20 | 21 | ret = [m for m in matches if self._match(base,m)] 22 | 23 | if info['sort']: 24 | # in python, 'A' sort's before 'a', we need to swapcase for the 'a' 25 | # sorting before 'A' 26 | ret.sort(key=lambda e: e['word'].swapcase()) 27 | 28 | return ret 29 | 30 | def _match(self,base,item): 31 | if len(base)>len(item['word']): 32 | return False 33 | for a,b in zip(base,item['word']): 34 | if not self._chcmp(a,b): 35 | return False 36 | return True 37 | 38 | -------------------------------------------------------------------------------- /pythonx/cm_matchers/substr_matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Matcher(object): 5 | 6 | def __init__(self,nvim,chcmp,*args): 7 | self._chcmp = chcmp 8 | 9 | def process(self,info,ctx,startcol,matches): 10 | 11 | # fix for chinese characters 12 | # `你好 abcd|` 13 | # has col('.')==11 on vim 14 | # the evaluated startcol is: startcol[8] typed[你好 abcd] 15 | # but in python, "你好 abcd"[8] is not a valid index 16 | begin = -(ctx['col'] - startcol) 17 | base = '' 18 | if begin: 19 | base = ctx['typed'][begin:] 20 | 21 | 22 | tmp = [] 23 | for item in matches: 24 | score = self._match(base,item) 25 | if score is None: 26 | continue 27 | tmp.append((item,score)) 28 | 29 | if info['sort']: 30 | # sort by score, the smaller the better 31 | tmp.sort(key=lambda e: e[1]) 32 | 33 | return [e[0] for e in tmp] 34 | 35 | # return the score, the smaller the better 36 | def _match(self, base, item): 37 | 38 | word = item['word'] 39 | if len(base)>len(word): 40 | return None 41 | 42 | if len(base) == 0: 43 | return 0 44 | 45 | for i in range(len(word)-len(base) + 1): 46 | match = True 47 | for j in range(len(base)): 48 | if not self._chcmp(base[j], word[i+j]): 49 | match = False 50 | break 51 | if match: 52 | return i 53 | 54 | return None 55 | 56 | -------------------------------------------------------------------------------- /pythonx/cm_scopers/html_scoper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import logging 4 | import copy 5 | from cm import Base, getLogger 6 | 7 | logger = getLogger(__name__) 8 | 9 | class Scoper(Base): 10 | 11 | scopes = ['html','xhtml','php','blade','jinja','jinja2','vue.html.javascript.css','vue'] 12 | 13 | def sub_context(self,ctx,src): 14 | 15 | lnum = ctx['lnum'] 16 | col = ctx['col'] 17 | from html.parser import HTMLParser 18 | 19 | scoper = self 20 | 21 | class MyHTMLParser(HTMLParser): 22 | 23 | last_data_start = None 24 | last_data = None 25 | 26 | scope_info = None 27 | skip = False 28 | 29 | def handle_starttag(self,tag,attrs): 30 | 31 | self.skip = False 32 | 33 | if tag in ['style','script']: 34 | for attr in attrs: 35 | try: 36 | # avoid css completion for lang="stylus" 37 | if tag=='style' and attr[0]=='lang' and attr[1] and attr[1] not in ['css','scss']: 38 | self.skip = True 39 | return 40 | if tag=='style' and attr[0]=='type' and attr[1] and attr[1] not in ['text/css']: 41 | self.skip = True 42 | return 43 | if tag=='script' and attr[0]=='type' and attr[1] and attr[1] not in ['text/javascript']: 44 | self.skip = True 45 | return 46 | except: 47 | pass 48 | 49 | def handle_endtag(self, tag): 50 | 51 | if self.skip: 52 | return 53 | 54 | if tag in ['style','script']: 55 | 56 | startpos = self.last_data_start 57 | endpos = self.getpos() 58 | if ((startpos[0]lnum 63 | or (endpos[0]==lnum 64 | and endpos[1]>=col)) 65 | ): 66 | 67 | self.scope_info = {} 68 | self.scope_info['lnum'] = lnum-startpos[0]+1 69 | if lnum==startpos[0]: 70 | self.scope_info['col'] = col-(startpos[1]+1)+1 71 | else: 72 | self.scope_info['col']=col 73 | 74 | if tag=='script': 75 | self.scope_info['scope']='javascript' 76 | else: 77 | # style 78 | self.scope_info['scope']='css' 79 | 80 | self.scope_info['scope_offset']= scoper.get_pos(startpos[0],startpos[1]+1,src) 81 | self.scope_info['scope_len']=len(self.last_data) 82 | 83 | # offset as lnum, col format 84 | self.scope_info['scope_lnum']= startpos[0] 85 | # startpos[1] is zero based 86 | self.scope_info['scope_col']= startpos[1]+1 87 | 88 | 89 | def handle_data(self, data): 90 | self.last_data = data 91 | self.last_data_start = self.getpos() 92 | 93 | parser = MyHTMLParser() 94 | parser.feed(src) 95 | if parser.scope_info: 96 | 97 | new_ctx = copy.deepcopy(ctx) 98 | new_ctx['scope'] = parser.scope_info['scope'] 99 | new_ctx['lnum'] = parser.scope_info['lnum'] 100 | new_ctx['col'] = parser.scope_info['col'] 101 | 102 | new_ctx['scope_offset'] = parser.scope_info['scope_offset'] 103 | new_ctx['scope_len'] = parser.scope_info['scope_len'] 104 | new_ctx['scope_lnum'] = parser.scope_info['scope_lnum'] 105 | new_ctx['scope_col'] = parser.scope_info['scope_col'] 106 | 107 | return new_ctx 108 | 109 | 110 | pos = self.get_pos(lnum,col,src) 111 | # css completions for style='|' 112 | for match in re.finditer(r'style\s*=\s*("|\')(.*?)\1',src): 113 | if match.start(2)>pos: 114 | return 115 | if match.end(2)=pos 118 | new_src = match.group(2) 119 | 120 | new_ctx = copy.deepcopy(ctx) 121 | new_ctx['scope'] = 'css' 122 | 123 | new_ctx['scope_offset'] = match.start(2) 124 | new_ctx['scope_len'] = len(new_src) 125 | scope_lnum_col = self.get_lnum_col(match.start(2),src) 126 | new_ctx['scope_lnum'] = scope_lnum_col[0] 127 | new_ctx['scope_col'] = scope_lnum_col[1] 128 | 129 | sub_pos = pos - match.start(2) 130 | sub_lnum_col = self.get_lnum_col(sub_pos,new_src) 131 | new_ctx['lnum'] = sub_lnum_col[0] 132 | new_ctx['col'] = sub_lnum_col[1] 133 | return new_ctx 134 | 135 | return None 136 | 137 | 138 | -------------------------------------------------------------------------------- /pythonx/cm_scopers/markdown_scoper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import logging 4 | import copy 5 | from cm import Base, getLogger 6 | 7 | logger = getLogger(__name__) 8 | 9 | class Scoper(Base): 10 | 11 | scopes = ['markdown'] 12 | 13 | def sub_context(self,ctx,src): 14 | 15 | scope = None 16 | pos = self.get_pos(ctx['lnum'], ctx['col'], src) 17 | 18 | pat = re.compile( 19 | r'^ (`{3,}|~{3,}) \s* (\S+)? \s* \n' 20 | r'(.+?)' 21 | r'^ \1 \s* (?:\n+|$)', re.M | re.X | re.S) 22 | 23 | for m in pat.finditer(src): 24 | if m.start() > pos: 25 | break 26 | if m.group(2) and m.start(3) <= pos and m.end(3) > pos: 27 | scope = dict(src=m.group(3), 28 | pos=pos-m.start(3), 29 | scope_offset=m.start(3), 30 | scope=m.group(2)) 31 | break 32 | 33 | if not scope: 34 | return None 35 | 36 | new_pos = scope['pos'] 37 | new_src = scope['src'] 38 | p = 0 39 | for idx,line in enumerate(new_src.split("\n")): 40 | if (p<=new_pos) and (p+len(line)+1>new_pos): 41 | new_ctx = copy.deepcopy(ctx) 42 | new_ctx['scope'] = scope['scope'] 43 | new_ctx['lnum'] = idx+1 44 | new_ctx['col'] = new_pos-p+1 45 | new_ctx['scope_offset'] = scope['scope_offset'] 46 | new_ctx['scope_len'] = len(new_src) 47 | lnum_col = self.get_lnum_col(scope['scope_offset'],src) 48 | new_ctx['scope_lnum'] = lnum_col[0] 49 | new_ctx['scope_col'] = lnum_col[1] 50 | return new_ctx 51 | else: 52 | p += len(line)+1 53 | 54 | return None 55 | 56 | -------------------------------------------------------------------------------- /pythonx/cm_scopers/rst_scoper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import logging 4 | import copy 5 | from cm import Base, getLogger 6 | 7 | logger = getLogger(__name__) 8 | 9 | class Scoper(Base): 10 | 11 | scopes = ['rst'] 12 | 13 | def sub_context(self,ctx,src): 14 | 15 | scope = None 16 | pos = self.get_pos(ctx['lnum'], ctx['col'], src) 17 | 18 | # pat = re.compile( 19 | # r'^ (`{3,}|~{3,}) \s* (\S+)? \s* \n' 20 | # r'(.+?)' 21 | # r'^ \1 \s* (?:\n+|$)', re.M | re.X | re.S) 22 | 23 | pat = re.compile( 24 | r':: [ \t]* (\S+)? [ \t]* \n' 25 | r'(?:(?:[ \t]+ [^\n]* $ \n)+)?' 26 | r' [ \t]* \n' 27 | r'((( [ \t]+ [^\n]* $ )? \n)+)', re.M | re.X) 28 | 29 | for m in pat.finditer(src): 30 | if m.start() > pos: 31 | break 32 | if m.group(1) and m.start(2) <= pos and m.end(2) > pos: 33 | scope = dict(src=m.group(2), 34 | pos=pos-m.start(2), 35 | scope_offset=m.start(2), 36 | scope=m.group(1)) 37 | break 38 | 39 | if not scope: 40 | return None 41 | 42 | new_pos = scope['pos'] 43 | new_src = scope['src'] 44 | p = 0 45 | for idx,line in enumerate(new_src.split("\n")): 46 | if (p<=new_pos) and (p+len(line)+1>new_pos): 47 | new_ctx = copy.deepcopy(ctx) 48 | new_ctx['scope'] = scope['scope'] 49 | new_ctx['lnum'] = idx+1 50 | new_ctx['col'] = new_pos-p+1 51 | new_ctx['scope_offset'] = scope['scope_offset'] 52 | new_ctx['scope_len'] = len(new_src) 53 | lnum_col = self.get_lnum_col(scope['scope_offset'],src) 54 | new_ctx['scope_lnum'] = lnum_col[0] 55 | new_ctx['scope_col'] = lnum_col[1] 56 | return new_ctx 57 | else: 58 | p += len(line)+1 59 | 60 | return None 61 | 62 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_bufkeyword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # For debugging 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | 7 | from cm import register_source, getLogger, Base 8 | register_source(name='cm-bufkeyword', 9 | priority=5, 10 | abbreviation='Key', 11 | events=['InsertEnter', 'BufEnter'],) 12 | 13 | import re 14 | import cm_default 15 | 16 | logger = getLogger(__name__) 17 | 18 | class Source(Base): 19 | 20 | def __init__(self,nvim): 21 | super(Source,self).__init__(nvim) 22 | self._words = set() 23 | self._last_ctx = None 24 | self.refresh_keyword(nvim.eval('cm#context()')) 25 | 26 | def cm_event(self,event,ctx,*args): 27 | if self._last_ctx and (self._last_ctx['changedtick'] == ctx['changedtick']): 28 | return 29 | self._last_ctx = ctx 30 | self.refresh_keyword(ctx) 31 | 32 | def refresh_keyword(self,ctx,all=True,expand=50): 33 | word_pattern = cm_default.word_pattern(ctx) 34 | compiled = re.compile(word_pattern) 35 | logger.info('refreshing_keyword, word_pattern [%s]', word_pattern) 36 | 37 | buffer = self.nvim.current.buffer 38 | 39 | if all: 40 | self._words = set() 41 | begin = 0 42 | end = len(buffer) 43 | else: 44 | begin = max(ctx['lnum']-50,0) 45 | end = min(ctx['lnum']+50,len(buffer)) 46 | 47 | logger.info('keyword refresh begin, current count: %s', len(self._words)) 48 | 49 | cur_lnum = ctx['lnum'] 50 | cur_col = ctx['col'] 51 | 52 | step = 1000 53 | for num in range(begin,end,step): 54 | lines = buffer[num:num+step] 55 | # convert 0 base to 1 base 56 | lnum = num+1 57 | for line in lines: 58 | if lnum == cur_lnum: 59 | for word in compiled.finditer(line): 60 | span = word.span() 61 | # filter-out the word at current cursor 62 | if (cur_col>=span[0]+1) and (cur_col-1<=span[1]+1): 63 | continue 64 | self._words.add(word.group()) 65 | else: 66 | for word in compiled.finditer(line): 67 | self._words.add(word.group()) 68 | lnum += 1 69 | 70 | logger.info('keyword refresh complete, count: %s', len(self._words)) 71 | 72 | def cm_refresh(self,info,ctx): 73 | 74 | # incremental refresh 75 | self.refresh_keyword(ctx,False) 76 | 77 | matches = (dict(word=word,icase=1) for word in self._words) 78 | matches = self.matcher.process(info, ctx, ctx['startcol'], matches) 79 | 80 | self.complete(info, ctx, ctx['startcol'], matches) 81 | 82 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_filepath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging, use this command to start neovim: 4 | # 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | # 7 | # 8 | # Please register source before executing any other code, this allow cm_core to 9 | # read basic information about the source without loading the whole module, and 10 | # modules required by this module 11 | from cm import register_source, getLogger, Base 12 | register_source(name='cm-filepath', 13 | abbreviation='path', 14 | word_pattern=r'''([^\W]|[-.~%$])+''', 15 | cm_refresh_patterns=[ 16 | r'''(\.[/\\]+|[a-zA-Z]:\\+|~\/+)''', r'''([^\W]|[-.~%$]|[/\\])+[/\\]+'''], 17 | options=dict(path_pattern=r'''(([^\W]|[-.~%$]|[/\\])+)'''), 18 | sort=0, 19 | priority=6,) 20 | 21 | import os 22 | import re 23 | from neovim.api import Nvim 24 | 25 | 26 | class Source(Base): 27 | 28 | def __init__(self, nvim): 29 | super(Source, self).__init__(nvim) 30 | 31 | def cm_refresh(self, info, ctx): 32 | 33 | typed = ctx['typed'] 34 | filepath = ctx['filepath'] 35 | startcol = ctx['startcol'] 36 | 37 | pkw = re.search(info['options']['path_pattern'] + r'$', typed).group(0) 38 | 39 | dir = os.path.expandvars(pkw) 40 | dir = os.path.expanduser(dir) 41 | expanded = False 42 | if dir != pkw: 43 | expanded = True 44 | dir = os.path.dirname(dir) 45 | 46 | self.logger.debug('dir: %s', dir) 47 | 48 | bdirs = [] 49 | if filepath != "": 50 | curdir = os.path.dirname(filepath) 51 | bdirs.append(('buf', curdir), ) 52 | 53 | # full path of current file, current working dir 54 | cwd = self.nvim.call('getcwd') 55 | bdirs.append(('cwd', cwd), ) 56 | 57 | if pkw and pkw[0] != ".": 58 | bdirs.append(('root', "/")) 59 | 60 | seen = set() 61 | matches = [] 62 | for label, bdir in bdirs: 63 | joined_dir = os.path.join(bdir, dir.strip('/')) 64 | self.logger.debug('searching dir: %s', joined_dir) 65 | try: 66 | names = os.listdir(joined_dir) 67 | names.sort(key=lambda name: name.lower()) 68 | self.logger.debug('search result: %s', names) 69 | for name in names: 70 | p = os.path.join(joined_dir, name) 71 | if p in seen: 72 | continue 73 | seen.add(p) 74 | word = os.path.basename(p) 75 | menu = '~' + label 76 | if expanded: 77 | menu += '~ ' + p 78 | matches.append(dict(word=word, icase=1, menu=menu, dup=1)) 79 | except Exception as ex: 80 | self.logger.info('exception on listing joined_dir [%s], %s', joined_dir, ex) 81 | continue 82 | 83 | refresh = 0 84 | if len(matches) > 1024: 85 | refresh = 1 86 | # pre filtering 87 | matches = self.matcher.process(info, ctx, startcol, matches) 88 | matches = matches[0:1024] 89 | 90 | self.logger.debug('startcol: %s, matches: %s', startcol, matches) 91 | self.complete(info, ctx, ctx['startcol'], matches, refresh) 92 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_gocode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging, use this command to start neovim: 4 | # 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | # 7 | # 8 | # Please register source before executing any other code, this allow cm_core to 9 | # read basic information about the source without loading the whole module, and 10 | # modules required by this module 11 | from cm import register_source, getLogger, Base 12 | 13 | register_source(name='cm-gocode', 14 | priority=9, 15 | abbreviation='Go', 16 | word_pattern=r'[\w/]+', 17 | early_cache=1, 18 | scoping=True, 19 | scopes=['go'], 20 | cm_refresh_patterns=[r'\.'],) 21 | 22 | import re 23 | import subprocess 24 | import json 25 | 26 | logger = getLogger(__name__) 27 | 28 | 29 | class Source(Base): 30 | 31 | def __init__(self,nvim): 32 | super(Source,self).__init__(nvim) 33 | self._checked = False 34 | 35 | try: 36 | from distutils.spawn import find_executable 37 | # echoe does not work here 38 | if not find_executable("gocode"): 39 | self.message('error', "Can't find [gocode] binary. Please install gocode http://github.com/nsf/gocode") 40 | except: 41 | pass 42 | 43 | def get_pos(self, lnum , col, src): 44 | lines = src.split(b'\n') 45 | pos = 0 46 | for i in range(lnum-1): 47 | pos += len(lines[i])+1 48 | pos += col-1 49 | return pos 50 | 51 | def cm_refresh(self,info,ctx,*args): 52 | 53 | # Note: 54 | # 55 | # If you'r implementing you own source, and you want to get the content 56 | # of the file, Please use `cm.get_src()` instead of 57 | # `"\n".join(self._nvim.current.buffer[:])` 58 | 59 | src = self.get_src(ctx).encode('utf-8') 60 | filepath = ctx['filepath'] 61 | 62 | # convert lnum, col to offset 63 | offset = self.get_pos(ctx['lnum'],ctx['col'],src) 64 | 65 | # invoke gocode 66 | proc = subprocess.Popen(args=['gocode','-f','json','autocomplete', filepath,'%s' % offset], 67 | stdin=subprocess.PIPE, 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.DEVNULL) 70 | 71 | result, errs = proc.communicate(src,timeout=30) 72 | # result: [1, [{"class": "func", "name": "Print", "type": "func(a ...interface{}) (n int, err error)"}, ...]] 73 | result = json.loads(result.decode('utf-8')) 74 | logger.info("result %s", result) 75 | if not result: 76 | return 77 | 78 | completions = result[1] 79 | startcol = ctx['col'] - result[0] 80 | 81 | if startcol==ctx['col'] and re.match(r'\w', ctx['typed'][-1]): 82 | # workaround gocode bug when completion is triggered in a golang 83 | # string 84 | return 85 | 86 | if not completions: 87 | return 88 | 89 | matches = [] 90 | 91 | for complete in completions: 92 | 93 | # { 94 | # "class": "func", 95 | # "name": "Fprintln", 96 | # "type": "func(w !io!io.Writer, a ...interface{}) (n int, err error)" 97 | # }, 98 | 99 | item = dict(word=complete['name'], 100 | icase=1, 101 | dup=1, 102 | menu=complete.get('type',''), 103 | # info=complete.get('doc',''), 104 | ) 105 | 106 | matches.append(item) 107 | 108 | # snippet support 109 | if 'class' in complete and complete['class']=='func' and 'type' in complete: 110 | m = re.search(r'func\((.*?)\)',complete['type']) 111 | if not m: 112 | continue 113 | params = m.group(1) 114 | params = params.split(',') 115 | logger.info('snippet params: %s',params) 116 | snip_params = [] 117 | num = 1 118 | optional = '' 119 | for param in params: 120 | param = param.strip() 121 | if not param: 122 | logger.error("failed to process snippet for item: %s, param: %s", item, param) 123 | break 124 | name = param.split(' ')[0] 125 | if param.find('...')>=0: 126 | # optional args 127 | if num>1: 128 | optional += '${%s:, %s...}' % (num, name) 129 | else: 130 | optional += '${%s:%s...}' % (num, name) 131 | break 132 | snip_params.append("${%s:%s}" % (num,name)) 133 | num += 1 134 | 135 | item['snippet'] = item['word'] + '(' + ", ".join(snip_params) + optional + ')${0}' 136 | 137 | logger.info('startcol %s, matches %s', startcol, matches) 138 | self.complete(info, ctx, startcol, matches) 139 | 140 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_jedi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging 4 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 5 | 6 | from __future__ import absolute_import 7 | import os 8 | 9 | py = 'python3' 10 | 11 | # detect python2 12 | if 'VIRTUAL_ENV' in os.environ: 13 | py2 = os.path.join(os.environ['VIRTUAL_ENV'], 'bin', 'python2') 14 | if os.path.isfile(py2): 15 | py = 'python2' 16 | 17 | from cm import register_source, getLogger, Base 18 | register_source(name='cm-jedi', 19 | priority=9, 20 | abbreviation='Py', 21 | scoping=True, 22 | scopes=['python'], 23 | multi_thread=0, 24 | # disable early cache to minimize the issue of #116 25 | # early_cache=1, 26 | # The last two patterns is for displaying function signatures r'\(\s?', r',\s?' 27 | cm_refresh_patterns=[r'^(import|from).*\s', r'\.', r'\(\s?', r',\s?'], 28 | python=py,) 29 | 30 | import re 31 | import jedi 32 | 33 | logger = getLogger(__name__) 34 | 35 | class Source(Base): 36 | 37 | def __init__(self,nvim): 38 | Base.__init__(self, nvim) 39 | self._snippet_engine = nvim.vars['cm_completed_snippet_engine'] 40 | 41 | # workaround for #62 42 | try: 43 | import resource 44 | import psutil 45 | mem = psutil.virtual_memory() 46 | resource.setrlimit(resource.RLIMIT_DATA, (mem.total/3, resource.RLIM_INFINITY)) 47 | except Exception as ex: 48 | logger.exception("set RLIMIT_DATA failed. %s", ex) 49 | pass 50 | 51 | def cm_refresh(self,info,ctx,*args): 52 | 53 | path = ctx['filepath'] 54 | typed = ctx['typed'] 55 | 56 | # Ignore comment, also workaround jedi's bug #62 57 | if re.match(r'\s*#', typed): 58 | return 59 | 60 | src = self.get_src(ctx) 61 | if not src.strip(): 62 | # empty src may possibly block jedi execution, don't know why 63 | logger.info('ignore empty src [%s]', src) 64 | return 65 | 66 | logger.info('context [%s]', ctx) 67 | 68 | # logger.info('jedi.Script lnum[%s] curcol[%s] path[%s] [%s]', lnum,len(typed),path,src) 69 | script = jedi.Script(src, ctx['lnum'], len(ctx['typed']), path) 70 | 71 | signature_text = '' 72 | signature = None 73 | try: 74 | signatures = script.call_signatures() 75 | logger.info('signatures: %s', signatures) 76 | if len(signatures)>0: 77 | signature = signatures[-1] 78 | params=[param.description for param in signature.params] 79 | signature_text = signature.name + '(' + ', '.join(params) + ')' 80 | logger.info("signature: %s, name: %s", signature, signature.name) 81 | except Exception as ex: 82 | logger.exception("get signature text failed %s", signature_text) 83 | 84 | is_import = False 85 | if re.search(r'^\s*(from|import)', typed): 86 | is_import = True 87 | 88 | if re.search(r'^\s*(?!from|import).*?[(,]\s*$', typed): 89 | if signature_text: 90 | matches = [dict(word='',empty=1,abbr=signature_text,dup=1),] 91 | # refresh=True 92 | # call signature popup doesn't need to be cached by the framework 93 | self.complete(info, ctx, ctx['col'], matches, True) 94 | return 95 | 96 | completions = script.completions() 97 | logger.info('completions %s', completions) 98 | 99 | matches = [] 100 | 101 | for complete in completions: 102 | 103 | insert = complete.complete 104 | 105 | item = dict(word=ctx['base']+insert, 106 | icase=1, 107 | dup=1, 108 | menu=complete.description, 109 | info=complete.docstring() 110 | ) 111 | 112 | # Fix the user typed case 113 | if item['word'].lower()==complete.name.lower(): 114 | item['word'] = complete.name 115 | 116 | # snippet support 117 | try: 118 | if (complete.type == 'function' or complete.type == 'class'): 119 | self.render_snippet(item, complete, is_import) 120 | except Exception as ex: 121 | logger.exception("exception parsing snippet for item: %s, complete: %s", item, complete) 122 | 123 | matches.append(item) 124 | 125 | logger.info('matches %s', matches) 126 | # workaround upstream issue by letting refresh=True. #116 127 | self.complete(info, ctx, ctx['startcol'], matches) 128 | 129 | def render_snippet(self, item, complete, is_import): 130 | 131 | doc = complete.docstring() 132 | 133 | # This line has performance issue 134 | # https://github.com/roxma/nvim-completion-manager/issues/126 135 | # params = complete.params 136 | 137 | fundef = doc.split("\n")[0] 138 | 139 | params = re.search(r'(?:_method|' + re.escape(complete.name) + ')' + r'\((.*)\)', fundef) 140 | 141 | if params: 142 | item['menu'] = fundef 143 | 144 | logger.debug("building snippet [%s] type[%s] doc [%s]", item['word'], complete.type, doc) 145 | 146 | if params and not is_import: 147 | 148 | num = 1 149 | placeholders = [] 150 | snip_args = '' 151 | 152 | params = params.group(1) 153 | if params != '': 154 | params = params.split(',') 155 | cnt = 0 156 | for param in params: 157 | cnt += 1 158 | if "=" in param or "*" in param or param[0] == '[': 159 | break 160 | else: 161 | name = param.strip('[').strip(' ') 162 | 163 | # Note: this is not accurate 164 | if cnt==1 and (name=='self' or name=='cls'): 165 | continue 166 | 167 | ph = self.snippet_placeholder(num, name) 168 | placeholders.append(ph) 169 | num += 1 170 | 171 | # skip optional parameters 172 | if "[" in param: 173 | break 174 | 175 | snip_args = ', '.join(placeholders) 176 | if len(placeholders) == 0: 177 | # don't jump out of parentheses if function has 178 | # parameters 179 | snip_args = self.snippet_placeholder(1) 180 | 181 | ph0 = self.snippet_placeholder(0) 182 | snippet = '%s(%s)%s' % (item['word'], snip_args, ph0) 183 | 184 | item['snippet'] = snippet 185 | logger.debug('snippet: [%s] placeholders: %s', snippet, placeholders) 186 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_keyword_continue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging, use this command to start neovim: 4 | # 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | # 7 | # 8 | # Please register source before executing any other code, this allow cm_core to 9 | # read basic information about the source without loading the whole module, and 10 | # modules required by this module 11 | from cm import register_source, getLogger, Base 12 | 13 | # A completion source with CTRL-X CTRL-N like feature 14 | # 15 | # sort=0 for not using NCM's builtin sorting 16 | # auto_popup=0, this source is kinkd of heavy weight (scan all buffers) 17 | register_source(name='cm-keyword-continue', 18 | priority=5, 19 | abbreviation='', 20 | word_pattern=r'\w+', 21 | cm_refresh_length=0, 22 | auto_popup=0, 23 | sort=0,) 24 | 25 | import re 26 | import copy 27 | 28 | logger = getLogger(__name__) 29 | 30 | class Source(Base): 31 | 32 | def __init__(self,nvim): 33 | super().__init__(nvim) 34 | 35 | def cm_refresh(self,info,ctx,*args): 36 | 37 | force = ctx.get('force',False) 38 | 39 | compiled = re.compile(info['word_pattern']) 40 | 41 | typed = ctx['typed'] 42 | if typed.strip()=='' and not force: 43 | # At the beginning of the line, need force to trigger the popup, 44 | # Otherwise this will be annoying. 45 | return 46 | try: 47 | # fetch the previous line for better sorting 48 | last_line = self.nvim.current.buffer[ctx['lnum']-2] 49 | typed = last_line + '\n' + typed 50 | except: 51 | pass 52 | 53 | typed_words = re.findall(compiled,typed) 54 | 55 | if not typed_words: 56 | return 57 | 58 | prev_word = '' 59 | if ctx['base']=='': 60 | prev_word = typed_words[-1] 61 | prev_words = typed_words 62 | else: 63 | if len(typed_words)<2: 64 | return 65 | prev_word = typed_words[-2] 66 | prev_words = typed_words[0:-1] 67 | 68 | if not isinstance(prev_word,str): 69 | prev_word = prev_word[0] 70 | prev_words = [e[0] for e in prev_words] 71 | 72 | reversed_prev_words = list(reversed(prev_words)) 73 | matches = [] 74 | 75 | # rank for sorting 76 | def get_rank(word,span,line,last_line): 77 | prev = last_line+"\n"+line[0:span[0]] 78 | words = re.findall(compiled,prev) 79 | if not words: 80 | return 0 81 | if not isinstance(words[0],str): 82 | words = [e[0] for e in words] 83 | ret = 0 84 | reserved_words = list(reversed(words)) 85 | for z in zip(reversed_prev_words,reserved_words): 86 | if z[0].lower()==z[1].lower(): 87 | ret += 1 88 | else: 89 | break 90 | return ret 91 | 92 | def compact_hint(rest_of_line,maxlen): 93 | words = list(compiled.finditer(rest_of_line)) 94 | # to the last non word sequence 95 | words_in_range = [e for e in words if e.span()[0]<=maxlen] 96 | if not words_in_range: 97 | return '' 98 | end = words_in_range[-1].span()[0] 99 | return rest_of_line[0:end] 100 | 101 | lnum = ctx['lnum'] 102 | bufnr = self.nvim.current.buffer.number 103 | 104 | for buffer in self.nvim.buffers: 105 | 106 | this_bufnr = buffer.number 107 | def word_generator(): 108 | step = 500 109 | line_cnt = len(buffer) 110 | this_lnum = 1 111 | for i in range(0,min(line_cnt,5000),step): 112 | lines = buffer[i:i+step] 113 | last_line = '' 114 | for line in lines: 115 | try: 116 | if this_lnum==lnum and bufnr==this_bufnr: 117 | # pass current editting line 118 | continue 119 | for word in re.finditer(compiled,line): 120 | yield word.group(),word.span(),line,last_line 121 | finally: 122 | last_line = line 123 | this_lnum += 1 124 | 125 | try: 126 | tmp_prev_word = '' 127 | tmp_prev_span = (0,0) 128 | for word,span,line,last_line in word_generator(): 129 | if tmp_prev_word==prev_word: 130 | 131 | rest_of_line = line[span[0]:] 132 | 133 | if len(rest_of_line)<50: 134 | hint = rest_of_line 135 | else: 136 | hint = compact_hint(rest_of_line,50) 137 | 138 | rest_of_line_without_this = line[span[1]:] 139 | next_word = compiled.search(rest_of_line_without_this) 140 | if not next_word: 141 | next_non_word = rest_of_line_without_this 142 | else: 143 | next_non_word = rest_of_line_without_this[0: next_word.span()[0]] 144 | # word = word + next_non_word_sequence 145 | matches.append(dict(word=word + next_non_word, menu=hint, _rest_of_line=rest_of_line, _rank=get_rank(word,span,line,last_line))) 146 | tmp_prev_word = word 147 | tmp_prev_span = span 148 | except Exception as ex: 149 | logger.exception("Parsing buffer [%s] failed", buffer) 150 | 151 | # sort the result based on total match 152 | matches.sort(key=lambda e: e['_rank'], reverse=True) 153 | 154 | if not force: 155 | # filter by ranking 156 | matches = [e for e in matches if e['_rank']>=3 ] 157 | 158 | # filter the result here, so that the result of line completion will be 159 | # displayed properly 160 | matches = self.matcher.process(info, ctx, ctx['startcol'], matches) 161 | 162 | if matches: 163 | # add rest_of_line completion for the highest rank 164 | e = copy.deepcopy(matches[0]) 165 | # e['abbr'] = e['word'] + e['menu'] + '...' 166 | e['abbr'] = 'the rest> ' 167 | hint = e['menu'] 168 | if len(hint) < len(e['_rest_of_line']): 169 | hint += ' ...' 170 | e['menu'] = e['word'] + hint 171 | e['word'] = e['_rest_of_line'] 172 | matches.insert(1,e) 173 | 174 | # if not matches: 175 | # return 176 | 177 | logger.info('matches %s', matches) 178 | self.complete(info, ctx, ctx['startcol'], matches) 179 | 180 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_tags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # For debugging 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | 7 | from cm import register_source, getLogger, Base 8 | register_source(name='cm-tags', 9 | priority=6, 10 | abbreviation='Tag', 11 | events=['WinEnter'],) 12 | 13 | import os 14 | import re 15 | import logging 16 | import sys 17 | 18 | logger = getLogger(__name__) 19 | 20 | class Source(Base): 21 | 22 | def __init__(self,nvim): 23 | super().__init__(nvim) 24 | self._files = self.nvim.call('tagfiles') 25 | 26 | def cm_event(self,event,ctx,*args): 27 | if event=="WinEnter": 28 | self._files = self.nvim.call('tagfiles') 29 | 30 | def cm_refresh(self,info,ctx): 31 | 32 | tags = {} 33 | 34 | for file in self._files: 35 | try: 36 | for line in binary_search_lines_by_prefix(ctx['base'],file): 37 | fields = line.split("\t") 38 | if len(fields)<2: 39 | continue 40 | tags[fields[0]] = dict(word=fields[0],menu='Tag: '+fields[1]) 41 | except Exception as ex: 42 | logger.exception('binary_search_lines_by_prefix exception: %s', ex) 43 | 44 | # unique 45 | matches = list(tags.values()) 46 | 47 | # there are huge tag files sometimes, be careful here. it's meaningless 48 | # to have huge amout of results. 49 | refresh = False 50 | if len(matches)>100: 51 | matches = matches[0:100] 52 | refresh = True 53 | 54 | logger.info('matches len %s', len(matches)) 55 | 56 | self.complete(info, ctx, ctx['startcol'], matches, refresh) 57 | 58 | 59 | def binary_search_lines_by_prefix(prefix,filename): 60 | 61 | with open(filename,'r') as f: 62 | 63 | def yield_results(): 64 | while True: 65 | line = f.readline() 66 | if not line: 67 | return 68 | if line[:len(prefix)]==prefix: 69 | yield line 70 | else: 71 | return 72 | 73 | begin = 0 74 | f.seek(0,2) 75 | end = f.tell() 76 | 77 | while begin= prefix: 103 | if line2pos < end: 104 | end = line2pos 105 | else: 106 | # (begin) ... | line0 int((begin+end)/2) | line1 (end) | line2 | 107 | # 108 | # this assignment push the middle_cursor forward, it may 109 | # also result in a case where begin==end 110 | # 111 | # do not use end = line1pos, may results in infinite loop 112 | end = int((begin+end)/2) 113 | if end==begin: 114 | if key1 == prefix: 115 | # find success 116 | f.seek(line2pos,0) 117 | yield from yield_results() 118 | return 119 | elif key2 == prefix: 120 | # find success 121 | # key1 < prefix && next line key2 == prefix 122 | f.seek(line2pos,0) 123 | yield from yield_results() 124 | return 125 | elif key2 < prefix: 126 | begin = line2end 127 | # if begin==end, then exit the loop 128 | else: 129 | # key1 < prefix && next line key2 > prefix here, not found 130 | return 131 | -------------------------------------------------------------------------------- /pythonx/cm_sources/cm_tmux.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # For debugging 5 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 6 | 7 | from cm import register_source, getLogger, Base 8 | import os 9 | register_source(name='cm-tmux', 10 | abbreviation='Tmux', 11 | priority=4, 12 | enable= 'TMUX' in os.environ, 13 | events=['CursorHold','CursorHoldI','FocusGained','BufEnter'],) 14 | 15 | import os 16 | import re 17 | import logging 18 | import subprocess 19 | 20 | logger = getLogger(__name__) 21 | 22 | class Source(Base): 23 | 24 | def __init__(self,nvim): 25 | super().__init__(nvim) 26 | 27 | self._words = set() 28 | 29 | self._split_pattern = r'[^\w]+' 30 | self._kw_pattern = r'\w' 31 | 32 | self.refresh_keyword() 33 | 34 | def cm_event(self,event,ctx,*args): 35 | if event in ['CursorHold','CursorHoldI','FocusGained','BufEnter']: 36 | logger.info('refresh_keyword on event %s', event) 37 | self.refresh_keyword() 38 | 39 | 40 | def refresh_keyword(self): 41 | pat = re.compile(self._split_pattern) 42 | self._words = set() 43 | 44 | # tmux list-window -F '#{window_index}' 45 | # tmux capture-pane -p -t "$window_index.$pane_index" 46 | proc = subprocess.Popen(args=['tmux', 'list-window', '-F', '#{window_index}'], 47 | stdin=subprocess.PIPE, 48 | stdout=subprocess.PIPE, 49 | stderr=subprocess.PIPE) 50 | 51 | outs, errs = proc.communicate(timeout=15) 52 | window_indices = outs.decode('utf-8') 53 | logger.info('list-window: %s', window_indices) 54 | 55 | # parse windows 56 | panes = [] 57 | for win_index in window_indices.strip().split('\n'): 58 | proc = subprocess.Popen(args=['tmux', 'list-panes', '-t', win_index, '-F', '#{pane_index}'], 59 | stdin=subprocess.PIPE, 60 | stdout=subprocess.PIPE, 61 | stderr=subprocess.PIPE) 62 | outs, errs = proc.communicate(timeout=15) 63 | pane_ids = outs.decode('utf-8') 64 | 65 | for pane_id in pane_ids.strip().split('\n'): 66 | proc = subprocess.Popen(args=['tmux', 'capture-pane', '-p', '-t', '{}.{}'.format(win_index,pane_id)], 67 | stdin=subprocess.PIPE, 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE) 70 | outs, errs = proc.communicate(timeout=15) 71 | try: 72 | outs = outs.decode('utf-8') 73 | panes.append(outs) 74 | except Exception as ex: 75 | logger.exception('exception, failed to decode output, %s', ex) 76 | pass 77 | 78 | for pane in panes: 79 | for word in re.split(pat,pane): 80 | self._words.add(word) 81 | 82 | logger.info('keyword refresh complete, count: %s', len(self._words)) 83 | 84 | 85 | def cm_refresh(self,info,ctx): 86 | 87 | startcol = ctx['startcol'] 88 | 89 | matches = (dict(word=word,icase=1) for word in self._words) 90 | matches = self.matcher.process(info, ctx, startcol, matches) 91 | 92 | self.complete(info, ctx, ctx['startcol'], matches) 93 | 94 | -------------------------------------------------------------------------------- /pythonx/cm_start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # For debugging 4 | # NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO nvim 5 | 6 | import os 7 | import sys 8 | import importlib 9 | from neovim import attach, setup_logging 10 | from cm import getLogger, start_and_run_channel 11 | import atexit 12 | import threading 13 | import platform 14 | 15 | logger = getLogger(__name__) 16 | 17 | def main(): 18 | 19 | channel_type = sys.argv[1] 20 | 21 | # the default nice is inheriting from parent neovim process. Increment it 22 | # so that heavy calculation will not block the ui. 23 | try: 24 | os.nice(5) 25 | except: 26 | pass 27 | 28 | # psutil ionice 29 | try: 30 | import psutil 31 | p = psutil.Process(os.getpid()) 32 | p.ionice(psutil.IOPRIO_CLASS_IDLE) 33 | except: 34 | pass 35 | 36 | if channel_type == 'core': 37 | source_name = 'cm_core' 38 | modulename = 'cm_core' 39 | serveraddr = sys.argv[2] 40 | else: 41 | source_name = sys.argv[2] 42 | modulename = sys.argv[3] 43 | serveraddr = sys.argv[4] 44 | 45 | setup_logging(modulename) 46 | 47 | logger.info("start_channel for %s", modulename) 48 | 49 | # change proccess title 50 | try: 51 | import setproctitle 52 | setproctitle.setproctitle('%s nvim-completion-manager' % modulename) 53 | except: 54 | pass 55 | 56 | # Stop Popen from openning console window on Windows system 57 | if platform.system() == 'Windows': 58 | try: 59 | import subprocess 60 | cls = subprocess.Popen 61 | class NewPopen(cls): 62 | def __init__(self, *args, **keys): 63 | if 'startupinfo' not in keys: 64 | si = subprocess.STARTUPINFO() 65 | si.dwFlags |= subprocess.STARTF_USESHOWWINDOW 66 | keys['startupinfo'] = si 67 | cls.__init__(self, *args, **keys) 68 | subprocess.Popen = NewPopen 69 | except Exception as ex: 70 | logger.exception('Failed hacking subprocess.Popen for windows platform: %s', ex) 71 | 72 | try: 73 | start_and_run_channel(channel_type, serveraddr, source_name, modulename) 74 | except Exception as ex: 75 | logger.exception('Exception when running %s: %s', modulename, ex) 76 | exit(1) 77 | finally: 78 | # terminate here 79 | exit(0) 80 | 81 | main() 82 | 83 | --------------------------------------------------------------------------------