import sys import os from importlib import import_module import logging import platform import subprocess from subprocess import Popen from os import path import unicodedata from copy import deepcopy import json import time __all__ = ['Ncm2Base', 'Ncm2Source', 'Popen'] if platform.system() == 'Windows': cls = Popen # redefine popen class Popen(cls): def __init__(self, *args, **keys): if 'startupinfo' not in keys: si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW keys['startupinfo'] = si cls.__init__(self, *args, **keys) def getLogger(name): def get_loglevel(): # logging setup level = logging.INFO if 'NVIM_PYTHON_LOG_LEVEL' in os.environ: l = getattr(logging, os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(), level) if isinstance(l, int): level = l if 'NVIM_NCM2_LOG_LEVEL' in os.environ: l = getattr(logging, os.environ['NVIM_NCM2_LOG_LEVEL'].strip(), level) if isinstance(l, int): level = l return level logger = logging.getLogger(__name__) logger.setLevel(get_loglevel()) return logger logger = getLogger(__name__) def matcher_get(context, opt=None): if 'matcher' in context: if opt is None: opt = context['matcher'] elif opt is None: # FIXME This is only for backword compability opt = context context = {} name = opt['name'] modname = 'ncm2_matcher.' + name mod = import_module(modname) # Some matchers, e.g. equal matcher, need to disable the incremental # match feature. It needs a way to set inc_match=0. This is why we need # to pass context to the matcher. m = mod.Matcher(context=context, **opt) return m def matcher_opt_formalize(opt): if type(opt) is str: return dict(name=opt) return deepcopy(opt) class Ncm2Base: def __init__(self, nvim): self.nvim = nvim def matcher_opt_formalize(self, opt): return matcher_opt_formalize(opt) def matcher_get(self, context): return matcher_get(context) def match_formalize(self, ctx, item): e = {} if type(item) is str: e['word'] = item else: e = deepcopy(item) e['icase'] = 1 e['equal'] = 1 if 'menu' not in e or type(e['menu']) != str: e['menu'] = '' if 'info' not in e or type(e['info']) != str: e['info'] = '' if 'abbr' not in e or type(e['abbr']) != str: e['abbr'] = e['word'] if 'kind' not in e or type(e['kind']) != str: e['kind'] = '' # LanguageClient-neovim sends json-encoded user_data if type(e.get('user_data', None)) is str: try: e['user_data'] = json.loads(item['user_data']) except: pass if 'user_data' not in e or type(e['user_data']) != dict: e['user_data'] = {} ud = e['user_data'] ud['source'] = ctx['source'] ud['ncm2'] = 1 return e def matches_formalize(self, ctx, matches): formalized = [] for e in matches: formalized.append(self.match_formalize(ctx, e)) return formalized def lccol2pos(self, lnum, ccol, src): """ convert lnum, ccol into pos """ lines = src.splitlines() or [""] pos = 0 for i in range(lnum - 1): pos += len(lines[i]) + 1 pos += ccol - 1 return pos def pos2lccol(self, pos, src): """ convert pos into lnum, ccol """ lines = src.splitlines() or [""] p = 0 for idx, line in enumerate(lines): if p <= pos and p + len(line) >= pos: return (idx + 1, pos - p + 1) p += len(line) + 1 def get_src(self, src, ctx): """ Get the source code of current scope identified by the ctx object. """ bufnr = ctx['bufnr'] changedtick = ctx['changedtick'] scope_offset = ctx.get('scope_offset', 0) scope_len = ctx.get('scope_len', len(src)) return src[scope_offset: scope_offset + scope_len] def update_rtp(self, rtp): for ele in rtp.split(','): pyx = path.join(ele, 'pythonx') if pyx not in sys.path: sys.path.append(pyx) py3 = path.join(ele, 'python3') if py3 not in sys.path: sys.path.append(py3) def strdisplaywidth(self, s): def get_char_display_width(unicode_str): r = unicodedata.east_asian_width(unicode_str) if r == "F": # Fullwidth return 1 elif r == "H": # Half-width return 1 elif r == "W": # Wide return 2 elif r == "Na": # Narrow return 1 elif r == "A": # Ambiguous, go with 2 return 1 elif r == "N": # Neutral return 1 else: return 1 s = unicodedata.normalize('NFC', s) w = 0 for c in s: w += get_char_display_width(c) return w class Ncm2Source(Ncm2Base): def __init__(self, nvim): Ncm2Base.__init__(self, nvim) # add lazy_check_context to on_complete method on_complete_impl = self.on_complete def on_complete(context, *args): if not self.lazy_check_context(context): logger.info('on_complete lazy_check_context failed') return on_complete_impl(context, *args) self.on_complete = on_complete logger.debug('on_complete is wrapped') def lazy_check_context(self, context): if context.get('dated', 0): return False # only checks when we receives a context that seems old now = time.time() if now >= context['time'] + 0.5: return not self.nvim.call('ncm2#complete_context_dated', context) else: return True def complete(self, ctx, startccol, matches, refresh=False): self.nvim.call('ncm2#complete', ctx, startccol, matches, refresh, async_=True)