├── .gitignore ├── .gitmodules ├── README.md ├── autoload ├── async_clj_omni │ ├── cmp.vim │ ├── ncm2.vim │ └── sources.vim └── coc │ └── source │ └── async_clj_omni.vim ├── ncm2-plugin └── async_clj_omni.vim ├── plugin └── cmp_fireplace.vim ├── pythonx ├── async_clj_omni │ ├── __init__.py │ ├── acid.py │ ├── cider.py │ └── fireplace.py └── cm_sources │ ├── acid.py │ └── fireplace.py └── rplugin └── python3 └── deoplete └── sources ├── acid.py └── async_clj.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rplugin/python3/deoplete/sources/vim_nrepl_python_client"] 2 | path = rplugin/python3/deoplete/sources/vim_nrepl_python_client 3 | url = https://github.com/clojure-vim/nrepl-python-client.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-async.nvim 2 | 3 | Provides async clojure completion for: 4 | 5 | * [deoplete.nvim][] 6 | * [ncm2][] 7 | * [asyncomplete.vim][] 8 | * [coc.nvim][] 9 | * [nvim-cmp][] 10 | 11 | Trying to use Fireplace's omnicompletion with auto-complete is painfully 12 | slow at times, making typing blocked. Using this module will be faster as 13 | it does not block, it runs in it's own thread. 14 | 15 | ## Installation 16 | 17 | ### CIDER 18 | 19 | For this plugin to work, your nREPL must have CIDER available. You can install it for [lein](https://github.com/clojure-emacs/cider-nrepl#via-leiningen) and [boot](https://github.com/boot-clj/boot/wiki/Cider-REPL). 20 | 21 | ### Deoplete 22 | 23 | Follow the install instructions for [deoplete.nvim][]. Then just include with 24 | your favourite plugin manager, mine is [vim-plug][] 25 | 26 | ```vim 27 | Plug 'clojure-vim/async-clj-omni' 28 | ``` 29 | 30 | You also need to include the following line in your init.vim: 31 | 32 | ```vim 33 | call deoplete#custom#option('keyword_patterns', {'clojure': '[\w!$%&*+/:<=>?@\^_~\-\.#]*'}) 34 | ``` 35 | 36 | ### Nvim Completion Manager 2 37 | 38 | 1. Follow the install instructions for [ncm2][]. 39 | 2. Add this plugin using your favourite plugin manager, 40 | ```vim 41 | Plug 'clojure-vim/async-clj-omni' 42 | ``` 43 | 44 | ### asyncomplete.vim 45 | 46 | Registration: 47 | 48 | ``` 49 | au User asyncomplete_setup call asyncomplete#register_source({ 50 | \ 'name': 'async_clj_omni', 51 | \ 'whitelist': ['clojure'], 52 | \ 'completor': function('async_clj_omni#sources#complete'), 53 | \ }) 54 | ``` 55 | 56 | ### coc.nvim 57 | 58 | 1. Follow the install instructions for [coc.nvim][]. 59 | 2. Add this plugin using your favourite plugin manager, 60 | ```vim 61 | Plug 'clojure-vim/async-clj-omni' 62 | ``` 63 | 64 | ### nvim-cmp 65 | 66 | 1. Follow the install instructions for [nvim-cmp][]. 67 | 2. Add `{ name = 'async_clj_omni' }`, a complete example: 68 | ```lua 69 | cmp.setup({ 70 | sources = { 71 | { name = 'async_clj_omni' }, 72 | } 73 | }) 74 | ``` 75 | 76 | ## Developing 77 | 78 | ### Deoplete 79 | A few snippets and tidbits for development: 80 | 81 | ```vimscript 82 | :call deoplete#custom#set('async_clj', 'debug_enabled', 1) 83 | :call deoplete#enable_logging("DEBUG", "/tmp/deopletelog") 84 | ``` 85 | 86 | Then you can this command to watch debug statements: 87 | ```bash 88 | $ tail -f /tmp/deopletelog 89 | ``` 90 | 91 | Debug statements can be made in the source via: 92 | ```python 93 | self.debug(msg) 94 | ``` 95 | 96 | ### Nvim Completion Manager 97 | 98 | ``` 99 | NVIM_PYTHON_LOG_FILE=logfile NVIM_PYTHON_LOG_LEVEL=DEBUG nvim 100 | ``` 101 | 102 | ## FAQ 103 | 104 | 1. Why do you include [nrepl-python-client][] via submodule. 105 | 106 | I made the decision that it was more complex to have users try and manage a 107 | version of [nrepl-python-client][], than it was for them to "just" have it 108 | included. In an ideal world, I'd be able to use virtualenv with the 109 | Python/Neovim, but this isn't currently a realistic expectation for all 110 | users to be able to use. 111 | 112 | 113 | [deoplete.nvim]: https://github.com/Shougo/deoplete.nvim 114 | [nrepl-python-client]: https://github.com/clojure-vim/nrepl-python-client 115 | [vim-plug]: https://github.com/junegunn/vim-plug 116 | [ncm]: https://github.com/roxma/nvim-completion-manager 117 | [ncm2]: https://github.com/ncm2/ncm2 118 | [coc.nvim]: https://github.com/neoclide/coc.nvim 119 | [asyncomplete.vim]: https://github.com/prabirshrestha/asyncomplete.vim 120 | [nvim-cmp]: https://github.com/hrsh7th/nvim-cmp 121 | -------------------------------------------------------------------------------- /autoload/async_clj_omni/cmp.vim: -------------------------------------------------------------------------------- 1 | let s:source = {} 2 | 3 | function! s:source.new() abort 4 | return deepcopy(s:source) 5 | endfunction 6 | 7 | function! s:source.is_available() 8 | if fireplace#op_available('complete') 9 | return v:true 10 | else 11 | return v:false 12 | endif 13 | endfunction 14 | 15 | function! s:source.get_keyword_pattern(params) 16 | " Minimum 2 letters because completion on "y" doesn't resolve any 17 | " namespaces, but "ya" will resolve on "yada". 18 | return '\k\k\+' 19 | endfunction 20 | 21 | function! s:source.get_trigger_characters(params) 22 | return ['/', '.', ':'] 23 | endfunction 24 | 25 | " unfortunately f is for both function & static method. to workaround, we'll 26 | " need to go to a lower level than fireplace#omnicomplete, which would lose us 27 | " the context of the completion from surrounding lines. 28 | let s:lsp_kinds = luaeval(" 29 | \ (function() 30 | \ local cmp = require'cmp' 31 | \ return {f = cmp.lsp.CompletionItemKind.Function, 32 | \ m = cmp.lsp.CompletionItemKind.Function, 33 | \ v = cmp.lsp.CompletionItemKind.Variable, 34 | \ s = cmp.lsp.CompletionItemKind.Keyword, 35 | \ c = cmp.lsp.CompletionItemKind.Class, 36 | \ k = cmp.lsp.CompletionItemKind.Keyword, 37 | \ l = cmp.lsp.CompletionItemKind.Variable, 38 | \ n = cmp.lsp.CompletionItemKind.Module, 39 | \ i = cmp.lsp.CompletionItemKind.Field, 40 | \ r = cmp.lsp.CompletionItemKind.File,} 41 | \ end)()") 42 | 43 | function! s:coerce_to_lsp(vc) 44 | return {'label': a:vc.word, 45 | \ 'labelDetails': { 46 | \ 'detail': a:vc.menu, 47 | \ }, 48 | \ 'documentation': a:vc.info, 49 | \ 'kind': get(s:lsp_kinds, a:vc.kind, 1) 50 | \ } 51 | endf 52 | 53 | function! s:source.complete(params, callback) abort 54 | let l:kw = a:params.context.cursor_before_line[(a:params.offset-1):] 55 | 56 | call fireplace#omnicomplete({candidates -> 57 | \ a:callback(map(candidates, 58 | \ {_, val -> s:coerce_to_lsp(val)})) 59 | \ }, 60 | \ l:kw) 61 | endfunction 62 | 63 | function async_clj_omni#cmp#register() 64 | call cmp#register_source('async_clj_omni', s:source.new()) 65 | endf 66 | -------------------------------------------------------------------------------- /autoload/async_clj_omni/ncm2.vim: -------------------------------------------------------------------------------- 1 | if exists('g:async_clj_omni_loaded_ncm2') 2 | finish 3 | endif 4 | let g:async_clj_omni_loaded_ncm2 = 1 5 | 6 | function! s:on_complete(c) 7 | call fireplace#omnicomplete({candidates -> 8 | \ ncm2#complete(a:c, a:c.startccol, candidates, 1) 9 | \ }, 10 | \ a:c.base) 11 | endf 12 | 13 | function! async_clj_omni#ncm2#init() 14 | call ncm2#register_source({ 15 | \ 'name': 'async_clj_omni', 16 | \ 'mark': 'clj', 17 | \ 'priority': 9, 18 | \ 'word_pattern': '[\w!$%&*+/:<=>?@\^_~\-\.#]+', 19 | \ 'complete_pattern': ['\.', '/'], 20 | \ 'complete_length': 0, 21 | \ 'matcher': 'none', 22 | \ 'scope': ['clojure'], 23 | \ 'on_complete': function('on_complete'), 24 | \ }) 25 | endf 26 | -------------------------------------------------------------------------------- /autoload/async_clj_omni/sources.vim: -------------------------------------------------------------------------------- 1 | function! s:completor(opt, ctx) abort 2 | let l:col = a:ctx['col'] 3 | let l:typed = a:ctx['typed'] 4 | 5 | let l:kw = matchstr(l:typed, '\v[[:alnum:]!$%&*+/:<=>?@\^_~\-\.#]+$') 6 | let l:kwlen = len(l:kw) 7 | 8 | let l:startcol = l:col - l:kwlen 9 | 10 | call fireplace#omnicomplete({candidates -> 11 | \ asyncomplete#complete(a:opt['name'], a:ctx, l:startcol, candidates, 1)}, 12 | \ l:kw) 13 | endfunction 14 | 15 | function! async_clj_omni#sources#complete(opt, ctx) 16 | call s:completor(a:opt, a:ctx) 17 | endfunction 18 | -------------------------------------------------------------------------------- /autoload/coc/source/async_clj_omni.vim: -------------------------------------------------------------------------------- 1 | function! coc#source#async_clj_omni#init() abort 2 | return { 3 | \'shortcut': 'clj', 4 | \'priority': 1, 5 | \'filetypes': ['clojure'], 6 | \'firstMatch': 0, 7 | \'triggerCharacters': ['.', '/', ':'], 8 | \} 9 | endfunction 10 | 11 | function! coc#source#async_clj_omni#complete(opt, cb) abort 12 | call fireplace#omnicomplete({candidates -> 13 | \ a:cb(candidates) 14 | \ }, 15 | \ a:opt.input) 16 | endfunction 17 | -------------------------------------------------------------------------------- /ncm2-plugin/async_clj_omni.vim: -------------------------------------------------------------------------------- 1 | call async_clj_omni#ncm2#init() 2 | -------------------------------------------------------------------------------- /plugin/cmp_fireplace.vim: -------------------------------------------------------------------------------- 1 | augroup async_clj_omni_plugins 2 | autocmd! 3 | autocmd User CmpReady call async_clj_omni#cmp#register() 4 | augroup END 5 | -------------------------------------------------------------------------------- /pythonx/async_clj_omni/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-vim/async-clj-omni/1c683073d67f1397af4cd071b4136eb55cca47b7/pythonx/async_clj_omni/__init__.py -------------------------------------------------------------------------------- /pythonx/async_clj_omni/acid.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from async_clj_omni.cider import cider_gather # NOQA 3 | try: 4 | from acid.nvim import localhost, get_acid_ns 5 | from acid.session import SessionHandler, send 6 | loaded = True 7 | except: 8 | loaded = False 9 | 10 | 11 | class Acid_nrepl: 12 | def __init__(self, wc): 13 | self.wc = wc 14 | 15 | def send(self, msg): 16 | self.wc.send(msg) 17 | 18 | def watch(self, name, q, callback): 19 | self.wc.watch(name, q, callback) 20 | 21 | def unwatch(self, name): 22 | self.wc.unwatch(name) 23 | 24 | 25 | class AcidManager: 26 | def __init__(self, logger, vim): 27 | self._vim = vim 28 | self._logger = logger 29 | self.__conns = {} 30 | 31 | def on_init(self): 32 | if loaded: 33 | self.acid_sessions = SessionHandler() 34 | else: 35 | self._logger.debug('Acid.nvim not found. Please install it.') 36 | self.sessions = {} 37 | 38 | def get_wc(self, url): 39 | return self.acid_sessions.get_or_create(url) 40 | 41 | def get_session(self, url, wc): 42 | if url in self.sessions: 43 | return self.sessions[url] 44 | 45 | session_event = threading.Event() 46 | 47 | def clone_handler(msg, wc, key): 48 | wc.unwatch(key) 49 | self.sessions[url] = msg['new-session'] 50 | session_event.set() 51 | 52 | wc.watch('dyn-session', {'new-session': None}, clone_handler) 53 | wc.send({'op': 'clone'}) 54 | session_event.wait(0.5) 55 | 56 | return self.sessions[url] 57 | 58 | def gather_candidates(self, keyword): 59 | if not loaded: 60 | return [] 61 | 62 | address = localhost(self._vim) 63 | if address is None: 64 | return [] 65 | url = "nrepl://{}:{}".format(*address) 66 | wc = self.get_wc(url) 67 | session = self.get_session(url, wc) 68 | ns = get_acid_ns(self._vim) 69 | 70 | def global_watch(cmsg, cwc, ckey): 71 | self._logger.debug("Received message for {}".format(url)) 72 | self._logger.debug(cmsg) 73 | 74 | wc.watch('global_watch', {}, global_watch) 75 | 76 | return cider_gather(self._logger, 77 | Acid_nrepl(wc), 78 | keyword, 79 | session, 80 | ns) 81 | -------------------------------------------------------------------------------- /pythonx/async_clj_omni/cider.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import threading 3 | import logging 4 | 5 | short_types = { 6 | "function": "f", 7 | "macro": "m", 8 | "var": "v", 9 | "special-form": "s", 10 | "class": "c", 11 | "keyword": "k", 12 | "local": "l", 13 | "namespace": "n", 14 | "field": "i", 15 | "method": "f", 16 | "static-field": "i", 17 | "static-method": "f", 18 | "resource": "r" 19 | } 20 | 21 | 22 | def candidate(val): 23 | arglists = val.get("arglists") 24 | type = val.get("type") 25 | return { 26 | "word": val.get("candidate"), 27 | "kind": short_types.get(type, type), 28 | "info": val.get("doc", ""), 29 | "menu": " ".join(arglists) if arglists else "" 30 | } 31 | 32 | def rethrow(e): 33 | raise e 34 | 35 | def cider_gather(logger, nrepl, keyword, session, ns, on_error=rethrow): 36 | # Should be unique for EVERY message 37 | msgid = uuid.uuid4().hex 38 | 39 | completion_event = threading.Event() 40 | response = None 41 | 42 | logger.debug("cider gather been called {}".format(msgid)) 43 | 44 | def completion_callback(cmsg, cwc, ckey): 45 | nonlocal response 46 | response = cmsg 47 | completion_event.set() 48 | 49 | nrepl.watch("{}-completion".format(msgid), 50 | {"id": msgid}, 51 | completion_callback) 52 | 53 | logger.debug("cider_gather watching msgid") 54 | 55 | try: 56 | # TODO: context for context aware completions 57 | nrepl.send({ 58 | "id": msgid, 59 | "op": "complete", 60 | "session": session, 61 | "symbol": keyword, 62 | "extra-metadata": ["arglists", "doc"], 63 | "ns": ns 64 | }) 65 | except BrokenPipeError as e: 66 | on_error(e) 67 | 68 | completion_event.wait(0.5) 69 | 70 | logger.debug("finished waiting for completion to happen") 71 | logger.debug("response truthy? {}".format(bool(response))) 72 | 73 | nrepl.unwatch("{}-completion".format(msgid)) 74 | 75 | if response: 76 | return [candidate(x) for x in response.get("completions", [])] 77 | return [] 78 | -------------------------------------------------------------------------------- /pythonx/async_clj_omni/fireplace.py: -------------------------------------------------------------------------------- 1 | import nrepl 2 | from async_clj_omni.cider import cider_gather 3 | 4 | class Error(Exception): 5 | """Base class for exceptions in this module.""" 6 | pass 7 | 8 | def gather_conn_info(vim): 9 | try: 10 | client = vim.eval("fireplace#client()") 11 | except Exception: 12 | raise Error 13 | 14 | if client: 15 | connection = client.get("connection", {}) 16 | transport = connection.get("transport", client.get("transport")) 17 | 18 | if not transport: 19 | raise Error 20 | 21 | ns = "" 22 | try: 23 | ns = vim.eval("fireplace#ns()") 24 | except Exception: 25 | pass 26 | 27 | return [client, connection, transport, ns] 28 | 29 | class ConnManager: 30 | def __init__(self, logger): 31 | self.__conns = {} 32 | self.logger = logger 33 | 34 | def get_conn(self, conn_string): 35 | if conn_string not in self.__conns: 36 | conn = nrepl.connect(conn_string) 37 | 38 | def global_watch(cmsg, cwc, ckey): 39 | self.logger.debug("Received message for {}".format(conn_string)) 40 | self.logger.debug(cmsg) 41 | 42 | wc = nrepl.WatchableConnection(conn) 43 | self.__conns[conn_string] = wc 44 | wc.watch("global_watch", {}, global_watch) 45 | 46 | return self.__conns.get(conn_string) 47 | 48 | def remove_conn(self, conn_string): 49 | self.logger.debug( 50 | ("Connection to {} died. " 51 | "Removing the connection.").format(conn_string) 52 | ) 53 | self.__conns.pop(conn_string, None) 54 | 55 | class Fireplace_nrepl: 56 | def __init__(self, wc): 57 | self.wc = wc 58 | 59 | def send(self, msg): 60 | self.wc.send(msg) 61 | 62 | def watch(self, name, q, callback): 63 | self.wc.watch(name, q, callback) 64 | 65 | def unwatch(self, name): 66 | self.wc.unwatch(name) 67 | 68 | class CiderCompletionManager: 69 | def __init__(self, logger, vim): 70 | self.__connmanager = ConnManager(logger) 71 | self.__logger = logger 72 | self.__vim = vim 73 | 74 | def gather_candidates(self, complete_str): 75 | self.__logger.debug("Gathering candidates") 76 | 77 | try: 78 | client, connection, transport, ns = gather_conn_info(self.__vim) 79 | except Error: 80 | self.__logger.debug("Unable to get connection info") 81 | return [] 82 | 83 | host = transport.get("host") 84 | port = transport.get("port") 85 | 86 | conn_string = "nrepl://{}:{}".format(host, port) 87 | 88 | wc = self.__connmanager.get_conn(conn_string) 89 | 90 | def on_error(e): 91 | self.__connmanager.remove_conn(conn_string) 92 | 93 | return cider_gather(self.__logger, 94 | Fireplace_nrepl(wc), 95 | complete_str, 96 | connection.get("session"), 97 | ns, 98 | on_error) 99 | -------------------------------------------------------------------------------- /pythonx/cm_sources/acid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from cm import register_source, getLogger, Base 4 | register_source(name='acid', 5 | abbreviation='acid', 6 | scopes=['clojure'], 7 | word_pattern=r'[\w!$%&*+/:<=>?@\^_~\-\.#]+', 8 | cm_refresh_patterns=[r'/'], 9 | priority=9) 10 | 11 | import os 12 | import sys 13 | import re 14 | 15 | class Source(Base): 16 | def __init__(self,nvim): 17 | super(Source,self).__init__(nvim) 18 | # TODO Why is this necessary when it isn't in deoplete sources? 19 | sys.path.append(os.path.join(nvim.eval('globpath(&rtp,"rplugin/python3/acid",1)').split("\n")[0], "..")) 20 | from async_clj_omni import acid 21 | self._nvim = nvim 22 | self._acid_manager = acid.AcidManager(getLogger(__name__), nvim) 23 | self._acid_manager.on_init() 24 | 25 | def cm_refresh(self,info,ctx): 26 | getLogger(__name__).debug('Running a refresh…') 27 | matches = self._acid_manager.gather_candidates(ctx['base']) 28 | self._nvim.call('cm#complete', info['name'], ctx, ctx['startcol'], matches, async=True) 29 | -------------------------------------------------------------------------------- /pythonx/cm_sources/fireplace.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from cm import register_source, getLogger, Base 4 | register_source(name='fireplace', 5 | abbreviation='🔥', 6 | scopes=['clojure'], 7 | word_pattern=r'[\w!$%&*+/:<=>?@\^_~\-\.#]+', 8 | cm_refresh_patterns=[r'/'], 9 | priority=9) 10 | 11 | import sys 12 | import os 13 | basedir = os.path.dirname(os.path.realpath(__file__)) 14 | sys.path.append(os.path.normpath(os.path.join(basedir, "../../rplugin/python3/deoplete/sources/vim_nrepl_python_client"))) 15 | from async_clj_omni import fireplace 16 | import re 17 | 18 | class Source(Base): 19 | def __init__(self,nvim): 20 | super(Source,self).__init__(nvim) 21 | self._nvim = nvim 22 | self._cider_completion_manager = fireplace.CiderCompletionManager(getLogger(__name__), nvim) 23 | 24 | def cm_refresh(self,info,ctx): 25 | getLogger(__name__).debug('Running a refresh…') 26 | matches = self._cider_completion_manager.gather_candidates(ctx['base']) 27 | self._nvim.call('cm#complete', info['name'], ctx, ctx['startcol'], matches, False, async=True) 28 | -------------------------------------------------------------------------------- /rplugin/python3/deoplete/sources/acid.py: -------------------------------------------------------------------------------- 1 | import deoplete.logger 2 | # Adds a git submodule to the import path 3 | import sys 4 | import os 5 | basedir = os.path.dirname(os.path.realpath(__file__)) 6 | sys.path.append(os.path.join(basedir, "../../acid")) 7 | sys.path.append(os.path.join(basedir, "../../../../pythonx/")) 8 | 9 | from async_clj_omni.acid import Acid_nrepl, AcidManager 10 | from .base import Base # NOQA 11 | 12 | 13 | class Source(Base): 14 | def __init__(self, vim): 15 | Base.__init__(self, vim) 16 | self.name = "acid" 17 | self.mark = "[acid]" 18 | self.filetypes = ['clojure'] 19 | self.rank = 200 20 | self._vim = vim 21 | self._AcidManager = AcidManager(deoplete.logger.getLogger('acid_cider_completion_manager'), vim) 22 | 23 | def on_init(self, context): 24 | self._AcidManager.on_init() 25 | 26 | def gather_candidates(self, context): 27 | return self._AcidManager.gather_candidates(context["complete_str"]) 28 | -------------------------------------------------------------------------------- /rplugin/python3/deoplete/sources/async_clj.py: -------------------------------------------------------------------------------- 1 | # Adds a git submodule to the import path 2 | import sys 3 | import os 4 | basedir = os.path.dirname(os.path.realpath(__file__)) 5 | sys.path.append(os.path.join(basedir, "vim_nrepl_python_client/")) 6 | sys.path.append(os.path.join(basedir, "../../../../pythonx/")) 7 | 8 | # from async_clj_omni.cider import cider_gather # NOQA 9 | from async_clj_omni import fireplace 10 | from .base import Base # NOQA 11 | import deoplete.logger 12 | 13 | 14 | class Source(Base): 15 | def __init__(self, vim): 16 | Base.__init__(self, vim) 17 | self.name = "async_clj" 18 | self.mark = "CLJ" 19 | self.filetypes = ['clojure'] 20 | self.rank = 200 21 | self.__cider_completion_manager = fireplace.CiderCompletionManager(deoplete.logger.getLogger('fireplace_cider_completion_manager'), vim) 22 | 23 | def gather_candidates(self, context): 24 | return self.__cider_completion_manager.gather_candidates(context["complete_str"]) 25 | --------------------------------------------------------------------------------