Fixed vim and zsh

This commit is contained in:
2018-04-05 13:06:54 +02:00
parent f9db886bd3
commit 0331f6518a
2009 changed files with 256303 additions and 0 deletions

View File

@ -0,0 +1,64 @@
# ============================================================================
# FILE: __init__.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from importlib import find_loader
from deoplete.deoplete import Deoplete
if find_loader('yarp'):
import vim
else:
import neovim
vim = neovim
if 'neovim' in locals() and hasattr(neovim, 'plugin'):
# Neovim only
@neovim.plugin
class DeopleteHandlers(object):
def __init__(self, vim):
self._vim = vim
@neovim.function('_deoplete_init', sync=False)
def init_channel(self, args):
self._deoplete = Deoplete(self._vim)
@neovim.rpc_export('deoplete_enable_logging')
def enable_logging(self, context):
self._deoplete.enable_logging()
@neovim.rpc_export('deoplete_auto_completion_begin')
def auto_completion_begin(self, context):
self._deoplete.completion_begin(context)
@neovim.rpc_export('deoplete_manual_completion_begin')
def manual_completion_begin(self, context):
self._deoplete.completion_begin(context)
@neovim.rpc_export('deoplete_on_event')
def on_event(self, context):
self._deoplete.on_event(context)
if find_loader('yarp'):
global_deoplete = Deoplete(vim)
def deoplete_init():
pass
def deoplete_enable_logging(context):
global_deoplete.enable_logging()
def deoplete_auto_completion_begin(context):
global_deoplete.completion_begin(context)
def deoplete_manual_completion_begin(context):
global_deoplete.completion_begin(context)
def deoplete_on_event(context):
global_deoplete.on_event(context)

View File

@ -0,0 +1,472 @@
# ============================================================================
# FILE: child.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import copy
import os.path
import re
import sys
import time
import msgpack
from collections import defaultdict
import deoplete.source # noqa
import deoplete.filter # noqa
from deoplete import logger
from deoplete.exceptions import SourceInitError
from deoplete.util import (bytepos2charpos, charpos2bytepos, error, error_tb,
import_plugin,
get_buffer_config, get_custom,
get_syn_names, convert2candidates, uniq_list_dict)
class Child(logger.LoggingMixin):
def __init__(self, vim):
self.name = 'child'
self._vim = vim
self._filters = {}
self._sources = {}
self._custom = []
self._profile_flag = None
self._profile_start_time = 0
self._loaded_sources = {}
self._loaded_filters = {}
self._source_errors = defaultdict(int)
self._prev_results = {}
self._unpacker = msgpack.Unpacker(
encoding='utf-8',
unicode_errors='surrogateescape')
self._packer = msgpack.Packer(
use_bin_type=True,
encoding='utf-8',
unicode_errors='surrogateescape')
self._ignore_sources = []
def main_loop(self, stdout):
while True:
feed = sys.stdin.buffer.raw.read(102400)
if feed is None:
continue
if feed == b'':
# EOF
return
self._unpacker.feed(feed)
self.debug('_read: %d bytes', len(feed))
for child_in in self._unpacker:
name = child_in['name']
args = child_in['args']
queue_id = child_in['queue_id']
self.debug('main_loop: %s begin', name)
ret = self.main(name, args, queue_id)
if ret:
self._write(stdout, ret)
self.debug('main_loop: end')
def main(self, name, args, queue_id):
ret = None
if name == 'enable_logging':
self._enable_logging()
elif name == 'add_source':
self._add_source(args[0])
elif name == 'add_filter':
self._add_filter(args[0])
elif name == 'set_source_attributes':
self._set_source_attributes(args[0])
elif name == 'set_custom':
self._set_custom(args[0])
elif name == 'on_event':
self._on_event(args[0])
elif name == 'merge_results':
ret = self._merge_results(args[0], queue_id)
return ret
def _write(self, stdout, expr):
stdout.buffer.write(self._packer.pack(expr))
stdout.flush()
def _enable_logging(self):
logging = self._vim.vars['deoplete#_logging']
logger.setup(self._vim, logging['level'], logging['logfile'])
self.is_debug_enabled = True
def _add_source(self, path):
source = None
try:
Source = import_plugin(path, 'source', 'Source')
if not Source:
return
source = Source(self._vim)
name = os.path.splitext(os.path.basename(path))[0]
source.name = getattr(source, 'name', name)
source.path = path
if source.name in self._loaded_sources:
# Duplicated name
error_tb(self._vim, 'Duplicated source: %s' % source.name)
error_tb(self._vim, 'path: "%s" "%s"' %
(path, self._loaded_sources[source.name]))
source = None
except Exception:
error_tb(self._vim, 'Could not load source: %s' % path)
finally:
if source:
self._loaded_sources[source.name] = path
self._sources[source.name] = source
self.debug('Loaded Source: %s (%s)', source.name, path)
def _add_filter(self, path):
f = None
try:
Filter = import_plugin(path, 'filter', 'Filter')
if not Filter:
return
f = Filter(self._vim)
name = os.path.splitext(os.path.basename(path))[0]
f.name = getattr(f, 'name', name)
f.path = path
if f.name in self._loaded_filters:
# Duplicated name
error_tb(self._vim, 'duplicated filter: %s' % f.name)
error_tb(self._vim, 'path: "%s" "%s"' %
(path, self._loaded_filters[f.name]))
f = None
except Exception:
# Exception occurred when loading a filter. Log stack trace.
error_tb(self._vim, 'Could not load filter: %s' % path)
finally:
if f:
self._loaded_filters[f.name] = path
self._filters[f.name] = f
self.debug('Loaded Filter: %s (%s)', f.name, path)
def _merge_results(self, context, queue_id):
self.debug('merged_results: begin')
results = self._gather_results(context)
merged_results = []
for result in [x for x in results
if not self._is_skip(x['context'], x['source'])]:
if self._update_result(result, context['input']):
rank = get_custom(self._custom,
result['source'].name, 'rank',
result['source'].rank)
dup = bool(result['source'].filetypes)
candidates = result['candidates']
# Note: cannot use set() for dict
if dup:
# Remove duplicates
candidates = uniq_list_dict(candidates)
merged_results.append({
'complete_position': result['complete_position'],
'mark': result['source'].mark,
'dup': dup,
'candidates': candidates,
'source_name': result['source'].name,
'rank': rank,
})
is_async = len([x for x in results if x['is_async']]) > 0
self.debug('merged_results: end')
return {
'queue_id': queue_id,
'is_async': is_async,
'merged_results': merged_results,
}
def _gather_results(self, context):
results = []
for source in [x[1] for x in self._itersource(context)]:
try:
if source.disabled_syntaxes and 'syntax_names' not in context:
context['syntax_names'] = get_syn_names(self._vim)
ctx = copy.deepcopy(context)
charpos = source.get_complete_position(ctx)
if charpos >= 0 and source.is_bytepos:
charpos = bytepos2charpos(
ctx['encoding'], ctx['input'], charpos)
ctx['char_position'] = charpos
ctx['complete_position'] = charpos2bytepos(
ctx['encoding'], ctx['input'], charpos)
ctx['complete_str'] = ctx['input'][ctx['char_position']:]
if charpos < 0 or self._is_skip(ctx, source):
if source.name in self._prev_results:
self._prev_results.pop(source.name)
# Skip
continue
if (source.name in self._prev_results and
self._use_previous_result(
context, self._prev_results[source.name],
source.is_volatile)):
results.append(self._prev_results[source.name])
continue
ctx['is_async'] = False
ctx['is_refresh'] = True
ctx['max_abbr_width'] = min(source.max_abbr_width,
ctx['max_abbr_width'])
ctx['max_kind_width'] = min(source.max_kind_width,
ctx['max_kind_width'])
ctx['max_menu_width'] = min(source.max_menu_width,
ctx['max_menu_width'])
if ctx['max_abbr_width'] > 0:
ctx['max_abbr_width'] = max(20, ctx['max_abbr_width'])
if ctx['max_kind_width'] > 0:
ctx['max_kind_width'] = max(10, ctx['max_kind_width'])
if ctx['max_menu_width'] > 0:
ctx['max_menu_width'] = max(10, ctx['max_menu_width'])
# Gathering
self._profile_start(ctx, source.name)
ctx['candidates'] = source.gather_candidates(ctx)
self._profile_end(source.name)
if ctx['candidates'] is None:
continue
ctx['candidates'] = convert2candidates(ctx['candidates'])
result = {
'name': source.name,
'source': source,
'context': ctx,
'is_async': ctx['is_async'],
'prev_linenr': ctx['position'][1],
'prev_input': ctx['input'],
'input': ctx['input'],
'complete_position': ctx['complete_position'],
'candidates': ctx['candidates'],
}
self._prev_results[source.name] = result
results.append(result)
except Exception:
self._source_errors[source.name] += 1
if source.is_silent:
continue
if self._source_errors[source.name] > 2:
error(self._vim, 'Too many errors from "%s". '
'This source is disabled until Neovim '
'is restarted.' % source.name)
self._ignore_sources.append(source.name)
continue
error_tb(self._vim, 'Errors from: %s' % source.name)
return results
def _gather_async_results(self, result, source):
try:
context = result['context']
context['is_refresh'] = False
async_candidates = source.gather_candidates(context)
result['is_async'] = context['is_async']
if async_candidates is None:
return
context['candidates'] += convert2candidates(async_candidates)
except Exception:
self._source_errors[source.name] += 1
if source.is_silent:
return
if self._source_errors[source.name] > 2:
error(self._vim, 'Too many errors from "%s". '
'This source is disabled until Neovim '
'is restarted.' % source.name)
self._ignore_sources.append(source.name)
else:
error_tb(self._vim, 'Errors from: %s' % source.name)
def _process_filter(self, f, context):
try:
self._profile_start(context, f.name)
if (isinstance(context['candidates'], dict) and
'sorted_candidates' in context['candidates']):
context_candidates = []
context['is_sorted'] = True
for candidates in context['candidates']['sorted_candidates']:
context['candidates'] = candidates
context_candidates += f.filter(context)
context['candidates'] = context_candidates
else:
context['candidates'] = f.filter(context)
self._profile_end(f.name)
except Exception:
error_tb(self._vim, 'Errors from: %s' % f)
def _update_result(self, result, context_input):
source = result['source']
# Gather async results
if result['is_async']:
self._gather_async_results(result, source)
if not result['candidates']:
return None
# Source context
ctx = copy.deepcopy(result['context'])
ctx['input'] = context_input
ctx['complete_str'] = context_input[ctx['char_position']:]
ctx['is_sorted'] = False
# Set ignorecase
case = ctx['smartcase'] or ctx['camelcase']
if case and re.search(r'[A-Z]', ctx['complete_str']):
ctx['ignorecase'] = 0
ignorecase = ctx['ignorecase']
# Filtering
for f in [self._filters[x] for x
in source.matchers + source.sorters + source.converters
if x in self._filters]:
self._process_filter(f, ctx)
ctx['ignorecase'] = ignorecase
# On post filter
if hasattr(source, 'on_post_filter'):
ctx['candidates'] = source.on_post_filter(ctx)
result['candidates'] = ctx['candidates']
return result if result['candidates'] else None
def _itersource(self, context):
filetypes = context['filetypes']
ignore_sources = set(self._ignore_sources)
for ft in filetypes:
ignore_sources.update(
get_buffer_config(context, ft,
'deoplete_ignore_sources',
'deoplete#ignore_sources',
{}))
for source_name, source in self._sources.items():
if source.filetypes is None or source_name in ignore_sources:
continue
if context['sources'] and source_name not in context['sources']:
continue
if source.filetypes and not any(x in filetypes
for x in source.filetypes):
continue
if not source.is_initialized and hasattr(source, 'on_init'):
self.debug('on_init Source: %s', source.name)
try:
source.on_init(context)
except Exception as exc:
if isinstance(exc, SourceInitError):
error(self._vim,
'Error when loading source {}: {}. '
'Ignoring.'.format(source_name, exc))
else:
error_tb(self._vim,
'Error when loading source {}: {}. '
'Ignoring.'.format(source_name, exc))
self._ignore_sources.append(source_name)
continue
else:
source.is_initialized = True
yield source_name, source
def _profile_start(self, context, name):
if self._profile_flag is 0 or not self.is_debug_enabled:
return
if not self._profile_flag:
self._profile_flag = context['vars']['deoplete#enable_profile']
if self._profile_flag:
return self._profile_start(context, name)
elif self._profile_flag:
self.debug('Profile Start: {0}'.format(name))
self._profile_start_time = time.clock()
def _profile_end(self, name):
if self._profile_start_time:
self.debug('Profile End : {0:<25} time={1:2.10f}'.format(
name, time.clock() - self._profile_start_time))
def _use_previous_result(self, context, result, is_volatile):
if context['position'][1] != result['prev_linenr']:
return False
if is_volatile:
return context['input'] == result['prev_input']
else:
return (re.sub(r'\w*$', '', context['input']) ==
re.sub(r'\w*$', '', result['prev_input']) and
context['input'].find(result['prev_input']) == 0)
def _is_skip(self, context, source):
if 'syntax_names' in context and source.disabled_syntaxes:
p = re.compile('(' + '|'.join(source.disabled_syntaxes) + ')$')
if next(filter(p.search, context['syntax_names']), None):
return True
if (source.input_pattern != '' and
re.search('(' + source.input_pattern + ')$',
context['input'])):
return False
if context['event'] == 'Manual':
return False
return not (source.min_pattern_length <=
len(context['complete_str']) <= source.max_pattern_length)
def _set_source_attributes(self, context):
"""Set source attributes from the context.
Each item in `attrs` is the attribute name. If the default value is in
context['vars'] under a different name, use a tuple.
"""
attrs = (
'filetypes',
'disabled_syntaxes',
'input_pattern',
('min_pattern_length', 'deoplete#auto_complete_start_length'),
'max_pattern_length',
('max_abbr_width', 'deoplete#max_abbr_width'),
('max_kind_width', 'deoplete#max_menu_width'),
('max_menu_width', 'deoplete#max_menu_width'),
'matchers',
'sorters',
'converters',
'mark',
'is_debug_enabled',
'is_silent',
)
for name, source in self._sources.items():
for attr in attrs:
if isinstance(attr, tuple):
default_val = context['vars'][attr[1]]
attr = attr[0]
else:
default_val = None
source_attr = getattr(source, attr, default_val)
setattr(source, attr, get_custom(context['custom'],
name, attr, source_attr))
def _set_custom(self, custom):
self._custom = custom
def _on_event(self, context):
for source_name, source in self._itersource(context):
if source.events is None or context['event'] in source.events:
self.debug('on_event: Source: %s', source_name)
try:
source.on_event(context)
except Exception as exc:
error_tb(self._vim, 'Exception during {}.on_event '
'for event {!r}: {}'.format(
source_name, context['event'], exc))

View File

@ -0,0 +1,212 @@
# ============================================================================
# FILE: deoplete.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from deoplete import logger
from deoplete.parent import Parent
from deoplete.util import (error_tb, find_rplugins, error)
class Deoplete(logger.LoggingMixin):
def __init__(self, vim):
self.name = 'core'
self._vim = vim
self._runtimepath = ''
self._custom = []
self._loaded_paths = set()
self._prev_merged_results = {}
self._prev_pos = []
self._parents = []
self._parent_count = 0
self._max_parents = max(
[1, self._vim.vars['deoplete#num_processes']])
if self._max_parents > 1 and not hasattr(self._vim, 'loop'):
error(self._vim, 'neovim-python 0.2.4+ is required.')
return
# Enable logging before "Init" for more information, and e.g.
# deoplete-jedi picks up the log filename from deoplete's handler in
# its on_init.
if self._vim.vars['deoplete#_logging']:
self.enable_logging()
# Init context
context = self._vim.call('deoplete#init#_context', 'Init', [])
context['rpc'] = 'deoplete_on_event'
# Init processes
for n in range(0, self._max_parents):
self._parents.append(Parent(vim, context))
if self._vim.vars['deoplete#_logging']:
for parent in self._parents:
parent.enable_logging()
# on_init() call
self.on_event(context)
if hasattr(self._vim, 'channel_id'):
self._vim.vars['deoplete#_channel_id'] = self._vim.channel_id
self._vim.vars['deoplete#_initialized'] = True
def enable_logging(self):
logging = self._vim.vars['deoplete#_logging']
logger.setup(self._vim, logging['level'], logging['logfile'])
self.is_debug_enabled = True
def completion_begin(self, context):
self.debug('completion_begin: %s', context['input'])
self.check_recache(context)
try:
is_async, position, candidates = self.merge_results(context)
except Exception:
error_tb(self._vim, 'Error while gathering completions')
is_async = False
position = -1
candidates = []
if is_async:
self._vim.call('deoplete#handler#_async_timer_start')
else:
self._vim.call('deoplete#handler#_async_timer_stop')
if not candidates and ('deoplete#_saved_completeopt'
in context['vars']):
self._vim.call('deoplete#mapping#_restore_completeopt')
# Async update is skipped if same.
prev_completion = context['vars']['deoplete#_prev_completion']
prev_candidates = prev_completion['candidates']
prev_pos = prev_completion['complete_position']
if (context['event'] == 'Async' and
prev_pos == self._vim.call('getpos', '.') and
prev_candidates and candidates == prev_candidates):
return
# error(self._vim, candidates)
self._vim.vars['deoplete#_context'] = {
'complete_position': position,
'candidates': candidates,
'event': context['event'],
'input': context['input'],
'is_async': is_async,
}
self._vim.call('deoplete#handler#_do_complete')
self.debug('completion_end: %s', context['input'])
def merge_results(self, context):
use_prev = context['position'] == self._prev_pos
if not use_prev:
self._prev_merged_results = {}
is_async = False
merged_results = []
for cnt, parent in enumerate(self._parents):
if use_prev and cnt in self._prev_merged_results:
# Use previous result
merged_results += self._prev_merged_results[cnt]
else:
result = parent.merge_results(context)
is_async = is_async or result[0]
if not result[0]:
self._prev_merged_results[cnt] = result[1]
merged_results += result[1]
self._prev_pos = context['position']
if not merged_results:
return (is_async, -1, [])
complete_position = min([x['complete_position']
for x in merged_results])
all_candidates = []
for result in sorted(merged_results,
key=lambda x: x['rank'], reverse=True):
candidates = result['candidates']
prefix = context['input'][
complete_position:result['complete_position']]
mark = result['mark'] + ' '
for candidate in candidates:
# Add prefix
candidate['word'] = prefix + candidate['word']
# Set default menu and icase
candidate['icase'] = 1
if (mark != ' ' and
candidate.get('menu', '').find(mark) != 0):
candidate['menu'] = mark + candidate.get('menu', '')
if result['dup']:
candidate['dup'] = 1
all_candidates += candidates
# self.debug(candidates)
max_list = context['vars']['deoplete#max_list']
if max_list > 0:
all_candidates = all_candidates[: max_list]
return (is_async, complete_position, all_candidates)
def load_sources(self, context):
# Load sources from runtimepath
for path in find_rplugins(context, 'source'):
if path in self._loaded_paths:
continue
self._loaded_paths.add(path)
self._parents[self._parent_count].add_source(path)
self.debug('Process %d: %s', self._parent_count, path)
self._parent_count += 1
self._parent_count %= self._max_parents
self.set_source_attributes(context)
self.set_custom(context)
def load_filters(self, context):
# Load filters from runtimepath
for path in find_rplugins(context, 'filter'):
if path in self._loaded_paths:
continue
self._loaded_paths.add(path)
for parent in self._parents:
parent.add_filter(path)
def set_source_attributes(self, context):
for parent in self._parents:
parent.set_source_attributes(context)
def set_custom(self, context):
self._custom = context['custom']
for parent in self._parents:
parent.set_custom(self._custom)
def check_recache(self, context):
if context['runtimepath'] != self._runtimepath:
self._runtimepath = context['runtimepath']
self.load_sources(context)
self.load_filters(context)
if context['rpc'] != 'deoplete_on_event':
self.on_event(context)
elif context['custom'] != self._custom:
self.set_source_attributes(context)
self.set_custom(context)
def on_event(self, context):
self.debug('on_event: %s', context['event'])
self.check_recache(context)
for parent in self._parents:
parent.on_event(context)

View File

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

View File

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

View File

@ -0,0 +1,20 @@
# ============================================================================
# FILE: base.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from abc import abstractmethod
from deoplete.logger import LoggingMixin
class Base(LoggingMixin):
def __init__(self, vim):
self.vim = vim
self.name = 'base'
self.description = ''
@abstractmethod
def filter(self, context):
pass

View File

@ -0,0 +1,32 @@
# ============================================================================
# FILE: converter_auto_delimiter.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from .base import Base
class Filter(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'converter_auto_delimiter'
self.description = 'auto delimiter converter'
def filter(self, context):
delimiters = context['vars']['deoplete#delimiters']
for candidate, delimiter in [
[x, last_find(x['abbr'], delimiters)]
for x in context['candidates']
if 'abbr' in x and x['abbr'] and
not last_find(x['word'], delimiters) and
last_find(x['abbr'], delimiters)]:
candidate['word'] += delimiter
return context['candidates']
def last_find(s, needles):
for needle in needles:
if len(s) >= len(needle) and s[-len(needle):] == needle:
return needle

View File

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

View File

@ -0,0 +1,39 @@
# ============================================================================
# FILE: converter_remove_overlap.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from .base import Base
class Filter(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'converter_remove_overlap'
self.description = 'remove overlap converter'
def filter(self, context):
if not context['next_input']:
return context['candidates']
m = re.match('\S+', context['next_input'])
if not m:
return context['candidates']
next = m.group(0)
for [overlap, candidate] in [
[x, y] for x, y
in [[overlap_length(x['word'], next), x]
for x in context['candidates']] if x > 0]:
if 'abbr' not in candidate:
candidate['abbr'] = candidate['word']
candidate['word'] = candidate['word'][: -overlap]
return context['candidates']
def overlap_length(left, right):
pos = len(right)
while pos > 0 and not left.endswith(right[: pos]):
pos -= 1
return pos

View File

@ -0,0 +1,23 @@
# ============================================================================
# FILE: converter_remove_paren.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from .base import Base
class Filter(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'converter_remove_paren'
self.description = 'remove parentheses converter'
def filter(self, context):
p = re.compile('\(\)?$')
for candidate in [x for x in context['candidates']
if p.search(x['word'])]:
candidate['word'] = re.sub('\(\)?$', '', candidate['word'])
return context['candidates']

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
# ============================================================================
# FILE: matcher_cpsm.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from .base import Base
import sys
import os
from deoplete.util import error, globruntime
class Filter(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'matcher_cpsm'
self.description = 'cpsm matcher'
self._initialized = False
self._disabled = False
def filter(self, context):
if not context['candidates'] or not context[
'input'] or self._disabled:
return context['candidates']
if not self._initialized:
# cpsm installation check
ext = '.pyd' if context['is_windows'] else '.so'
if globruntime(context['runtimepath'], 'bin/cpsm_py' + ext):
# Add path
sys.path.append(os.path.dirname(
globruntime(context['runtimepath'],
'bin/cpsm_py' + ext)[0]))
self._initialized = True
else:
error(self.vim, 'matcher_cpsm: bin/cpsm_py' + ext +
' is not found in your runtimepath.')
error(self.vim, 'matcher_cpsm: You must install/build' +
' Python3 support enabled cpsm.')
self._disabled = True
return []
cpsm_result = self._get_cpsm_result(
context['candidates'], context['complete_str'])
return [x for x in context['candidates']
if x['word'] in sorted(cpsm_result, key=cpsm_result.index)]
def _get_cpsm_result(self, candidates, pattern):
import cpsm_py
return cpsm_py.ctrlp_match((d['word'] for d in candidates),
pattern, limit=1000, ispath=False)[0]

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
# ============================================================================
# FILE: sorter_rank.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from .base import Base
class Filter(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'sorter_rank'
self.description = 'rank sorter'
def filter(self, context):
rank = context['vars']['deoplete#_rank']
complete_str = context['complete_str'].lower()
input_len = len(complete_str)
return sorted(context['candidates'],
key=lambda x: -1 * rank[x['word']]
if x['word'] in rank
else abs(x['word'].lower().find(
complete_str, 0, input_len)))

View File

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

View File

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

View File

@ -0,0 +1,137 @@
# ============================================================================
# FILE: parent.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import time
import os
import msgpack
import subprocess
from functools import partial
from queue import Queue
from deoplete import logger
from deoplete.process import Process
from deoplete.util import error_tb, error
dp_main = os.path.join(os.path.dirname(__file__), 'dp_main.py')
class Parent(logger.LoggingMixin):
def __init__(self, vim, context):
self.name = 'parent'
self._vim = vim
self._hnd = None
self._stdin = None
self._child = None
self._queue_id = ''
self._prev_pos = []
self._queue_in = Queue()
self._queue_out = Queue()
self._packer = msgpack.Packer(
use_bin_type=True,
encoding='utf-8',
unicode_errors='surrogateescape')
self._unpacker = msgpack.Unpacker(
encoding='utf-8',
unicode_errors='surrogateescape')
self._start_process(context)
def enable_logging(self):
self._put('enable_logging', [])
self.is_debug_enabled = True
def add_source(self, path):
self._put('add_source', [path])
def add_filter(self, path):
self._put('add_filter', [path])
def set_source_attributes(self, context):
self._put('set_source_attributes', [context])
def set_custom(self, custom):
self._put('set_custom', [custom])
def merge_results(self, context):
if self._child:
results = self._put('merge_results', [context])
else:
if context['position'] == self._prev_pos and self._queue_id:
# Use previous id
queue_id = self._queue_id
else:
queue_id = self._put('merge_results', [context])
if not queue_id:
return (False, [])
get = self._get(queue_id)
if not get:
# Skip the next merge_results
self._queue_id = queue_id
self._prev_pos = context['position']
return (True, [])
self._queue_id = ''
results = get[0]
return (results['is_async'],
results['merged_results']) if results else (False, [])
def on_event(self, context):
self._put('on_event', [context])
def _start_process(self, context):
if self._vim.vars['deoplete#num_processes'] > 1:
# Parallel
startupinfo = None
if os.name == 'nt':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self._hnd = self._vim.loop.create_task(
self._vim.loop.subprocess_exec(
partial(Process, self),
self._vim.vars.get('python3_host_prog', 'python3'),
dp_main,
self._vim.vars['deoplete#_serveraddr'],
stderr=None, cwd=context['cwd'], startupinfo=startupinfo))
else:
# Serial
from deoplete.child import Child
self._child = Child(self._vim)
def _put(self, name, args):
queue_id = str(time.time())
if self._child:
return self._child.main(name, args, queue_id)
if not self._hnd:
return None
msg = self._packer.pack({
'name': name, 'args': args, 'queue_id': queue_id
})
self._queue_in.put(msg)
if self._stdin:
try:
while not self._queue_in.empty():
self._stdin.write(self._queue_in.get_nowait())
except BrokenPipeError as e:
error_tb(self._vim, 'Crash in child process')
error(self._vim, 'stderr=' + str(self._proc.read_error()))
self._hnd = None
return queue_id
def _get(self, queue_id):
if not self._hnd:
return []
outs = []
while not self._queue_out.empty():
outs.append(self._queue_out.get_nowait())
return [x for x in outs if x['queue_id'] == queue_id]

View File

@ -0,0 +1,26 @@
# ============================================================================
# FILE: process.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import asyncio
class Process(asyncio.SubprocessProtocol):
def __init__(self, plugin):
self._plugin = plugin
self._vim = plugin._vim
def connection_made(self, transport):
self._plugin._stdin = transport.get_pipe_transport(0)
def pipe_data_received(self, fd, data):
unpacker = self._plugin._unpacker
unpacker.feed(data)
for child_out in unpacker:
self._plugin._queue_out.put(child_out)
def process_exited(self):
pass

View File

@ -0,0 +1,52 @@
# ============================================================================
# FILE: around.py
# AUTHOR: Khalidov Oleg <brooth at gmail.com>
# License: MIT license
# ============================================================================
import re
from .base import Base
from deoplete.util import parse_buffer_pattern, getlines
LINES_ABOVE = 20
LINES_BELOW = 20
class Source(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'around'
self.mark = '[~]'
self.rank = 300
def gather_candidates(self, context):
line = context['position'][1]
candidates = []
# lines above
words = parse_buffer_pattern(
reversed(getlines(self.vim, max([1, line - LINES_ABOVE]), line)),
context['keyword_patterns'])
candidates += [{'word': x, 'menu': 'A'} for x in words]
# grab ':changes' command output
p = re.compile(r'[\s\d]+')
lines = set()
for change_line in [x[p.search(x).span()[1]:] for x
in self.vim.call(
'execute', 'changes').split('\n')
if p.search(x)]:
if change_line and change_line != '-invalid-':
lines.add(change_line)
words = parse_buffer_pattern(lines, context['keyword_patterns'])
candidates += [{'word': x, 'menu': 'C'} for x in words]
# lines below
words = parse_buffer_pattern(
getlines(self.vim, line, line + LINES_BELOW),
context['keyword_patterns'])
candidates += [{'word': x, 'menu': 'B'} for x in words]
return candidates

View File

@ -0,0 +1,56 @@
# ============================================================================
# FILE: base.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import re
from abc import abstractmethod
from deoplete.logger import LoggingMixin
from deoplete.util import debug, error_vim
class Base(LoggingMixin):
def __init__(self, vim):
self.vim = vim
self.description = ''
self.mark = ''
self.max_pattern_length = 80
self.input_pattern = ''
self.matchers = ['matcher_fuzzy']
self.sorters = ['sorter_rank']
self.converters = [
'converter_remove_overlap',
'converter_truncate_abbr',
'converter_truncate_kind',
'converter_truncate_menu']
self.filetypes = []
self.debug_enabled = False
self.is_bytepos = False
self.is_initialized = False
self.is_volatile = False
self.is_silent = False
self.rank = 100
self.disabled_syntaxes = []
self.events = None
def get_complete_position(self, context):
m = re.search('(?:' + context['keyword_patterns'] + ')$',
context['input'])
return m.start() if m else -1
def print(self, expr):
if not self.is_silent:
debug(self.vim, expr)
def print_error(self, expr):
if not self.is_silent:
error_vim(self.vim, expr)
@abstractmethod
def gather_candidates(self, context):
pass
def on_event(self, context):
pass

View File

@ -0,0 +1,63 @@
# ============================================================================
# FILE: buffer.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from .base import Base
from deoplete.util import parse_buffer_pattern, getlines
class Source(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'buffer'
self.mark = '[B]'
self.events = ['Init', 'InsertEnter', 'BufWritePost']
self._limit = 1000000
self._buffers = {}
self._max_lines = 5000
def on_event(self, context):
if (context['bufnr'] not in self._buffers
or context['event'] == 'BufWritePost'):
self._make_cache(context)
def gather_candidates(self, context):
self.on_event(context)
tab_bufnrs = self.vim.call('tabpagebuflist')
same_filetype = context['vars'].get(
'deoplete#buffer#require_same_filetype', True)
return {'sorted_candidates': [
x['candidates'] for x in self._buffers.values()
if not same_filetype or
x['filetype'] in context['filetypes'] or
x['filetype'] in context['same_filetypes'] or
x['bufnr'] in tab_bufnrs
]}
def _make_cache(self, context):
# Bufsize check
size = self.vim.call('line2byte',
self.vim.call('line', '$') + 1) - 1
if size > self._limit:
return
try:
self._buffers[context['bufnr']] = {
'bufnr': context['bufnr'],
'filetype': self.vim.eval('&l:filetype'),
'candidates': [
{'word': x} for x in
sorted(parse_buffer_pattern(getlines(self.vim),
context['keyword_patterns']),
key=str.lower)
]
}
except UnicodeDecodeError:
return []

View File

@ -0,0 +1,51 @@
# ============================================================================
# FILE: dictionary.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from os.path import getmtime, exists
from collections import namedtuple
from .base import Base
DictCacheItem = namedtuple('DictCacheItem', 'mtime candidates')
class Source(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'dictionary'
self.mark = '[D]'
self.events = ['Init', 'InsertEnter']
self._cache = {}
def on_event(self, context):
self._make_cache(context)
def gather_candidates(self, context):
self._make_cache(context)
candidates = []
for filename in [x for x in self._get_dictionaries(context)
if x in self._cache]:
candidates.append(self._cache[filename].candidates)
return {'sorted_candidates': candidates}
def _make_cache(self, context):
for filename in self._get_dictionaries(context):
mtime = getmtime(filename)
if filename in self._cache and self._cache[
filename].mtime == mtime:
continue
with open(filename, 'r', errors='replace') as f:
self._cache[filename] = DictCacheItem(
mtime, [{'word': x} for x in sorted(
[x.strip() for x in f], key=str.lower)]
)
def _get_dictionaries(self, context):
return [x for x in context['dict__dictionary'].split(',')
if exists(x)]

View File

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

View File

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

View File

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

View File

@ -0,0 +1,156 @@
# ============================================================================
# FILE: tag.py
# AUTHOR: Felipe Morales <hel.sheep at gmail.com>
# Shougo Matsushita <Shougo.Matsu at gmail.com>
# Roxma <roxma at qq.com>
# License: MIT license
# ============================================================================
from .base import Base
import re
import os
from os.path import exists, getsize
class Source(Base):
def __init__(self, vim):
super().__init__(vim)
self.name = 'tag'
self.mark = '[T]'
def on_init(self, context):
self._limit = context['vars'].get(
'deoplete#tag#cache_limit_size', 500000)
def gather_candidates(self, context):
candidates = []
for filename in self._get_tagfiles(context):
for line in binary_search_lines_by_prefix(
context['complete_str'], filename):
candidate = self._make_candidate(line)
if candidate:
candidates.append(candidate)
return candidates
def _make_candidate(self, line):
cols = line.strip().split('\t', 2)
if not cols or cols[0].startswith('!_'):
return {}
tagfield = {}
if ';"' in cols[-1]:
cols[-1], fields = cols[-1].split(';"', 1)
for pair in fields.split('\t'):
if ':' not in pair:
tagfield['kind'] = pair
else:
k, v = pair.split(':', 1)
tagfield[k] = v
kind = tagfield.get('kind', '')
if kind == 'f':
i = cols[2].find('(')
if i != -1 and cols[2].find(')', i+1) != -1:
m = re.search(r'(\w+\(.*\))', cols[2])
if m:
return {'word': cols[0], 'abbr': m.group(1), 'kind': kind}
return {'word': cols[0], 'kind': kind}
def _get_tagfiles(self, context):
include_files = self.vim.call(
'neoinclude#include#get_tag_files') if self.vim.call(
'exists', '*neoinclude#include#get_tag_files') else []
return [x for x in self.vim.call(
'map', self.vim.call('tagfiles') + include_files,
'fnamemodify(v:val, ":p")')
if exists(x) and getsize(x) < self._limit]
def binary_search_lines_by_prefix(prefix, filename):
with open(filename, 'r', errors='ignore') as f:
# to properly keep bounds of this loop it's important to understand
# *exactly* what our variables mean.
#
# to make sure we process only full lines, we are going to always seek
# to file position (x - 1), then skip partial or full line found there.
# except for position 0 which we know belongs to full 1st line.
# each line (except 1st one) will have multiple corresponding seeking
# positions.
#
# we are interested in finding such a seeking position for the first
# line in the file that matches our prefix. (let's call it target)
# begin - guaranteed not to exceed our target position
# e.g. (begin <= target) at any time.
begin = 0
# end - guaranteed to be higher then at least one seeking position for
# the target. e.g. (target < end) at any time.
# Note that this means it can be below the actual target line
f.seek(0, os.SEEK_END)
end = f.tell()
while end > begin + 1:
# pos - current seeking position
pos = int((begin + end) / 2) - 1
if pos == 0:
f.seek(0, os.SEEK_SET)
else:
f.seek(pos - 1, os.SEEK_SET)
f.readline() # skip partial line
line = f.readline()
l2 = f.tell() # start of next line (or end of file)
if l2 == 1:
# this is a corner case of a single empty first line.
# we mast advance here or we'll have an endless loop
begin = 1
next
if line:
key = line[:len(prefix)]
if key < prefix:
# we are strictly before the target line.
# so it starts at least from l2
begin = max(begin, l2)
elif key > prefix:
# we are strictly past the target line.
# our target seeking position is less than current pos
end = pos
else:
# current line is a possible target. it's reachable from
# current 'pos', so `target pos is <= current pos`, or
# `target post < current post + 1`
end = min(end, pos + 1)
else:
# we reached end of file. our current seeking position doesn't
# correspond to any line
end = min(end, pos)
# now we are at a *seeking position* for the target line. need to skip
# to the actual line
if begin == 0:
f.seek(0, os.SEEK_SET)
else:
f.seek(begin - 1, os.SEEK_SET)
f.readline()
while True:
line = f.readline()
if line.startswith(prefix):
yield line
else:
break
return

View File

@ -0,0 +1,308 @@
# ============================================================================
# FILE: util.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import os
import re
import sys
import glob
import traceback
import unicodedata
from importlib.machinery import SourceFileLoader
def get_buffer_config(context, filetype, buffer_var, user_var, default_var):
if buffer_var in context['bufvars']:
return context['bufvars'][buffer_var]
ft = filetype if (filetype in context['vars'][user_var] or
filetype in default_var) else '_'
default = default_var.get(ft, '')
return context['vars'][user_var].get(ft, default)
def get_simple_buffer_config(context, buffer_var, user_var):
return (context['bufvars'][buffer_var]
if buffer_var in context['bufvars']
else context['vars'][user_var])
def set_pattern(variable, keys, pattern):
for key in keys.split(','):
variable[key] = pattern
def set_list(vim, variable, keys, list):
return vim.call('deoplete#util#set_pattern', variable, keys, list)
def set_default(vim, var, val):
return vim.call('deoplete#util#set_default', var, val)
def convert2list(expr):
return (expr if isinstance(expr, list) else [expr])
def convert2candidates(l):
return ([{'word': x} for x in l]
if l and isinstance(l, list) and isinstance(l[0], str) else l)
def globruntime(runtimepath, path):
ret = []
for rtp in re.split(',', runtimepath):
ret += glob.glob(rtp + '/' + path)
return ret
def find_rplugins(context, source):
"""Search for base.py or *.py
Searches $VIMRUNTIME/*/rplugin/python3/deoplete/$source[s]/
"""
rtp = context.get('runtimepath', '').split(',')
if not rtp:
return
sources = (
os.path.join('rplugin/python3/deoplete', source, 'base.py'),
os.path.join('rplugin/python3/deoplete', source, '*.py'),
os.path.join('rplugin/python3/deoplete', source + 's', '*.py'),
os.path.join('rplugin/python3/deoplete', source, '*', '*.py'),
)
for src in sources:
for path in rtp:
yield from glob.iglob(os.path.join(path, src))
def import_plugin(path, source, classname):
"""Import Deoplete plugin source class.
If the class exists, add its directory to sys.path.
"""
name = os.path.splitext(os.path.basename(path))[0]
module_name = 'deoplete.%s.%s' % (source, name)
module = SourceFileLoader(module_name, path).load_module()
cls = getattr(module, classname, None)
if not cls:
return None
dirname = os.path.dirname(path)
if dirname not in sys.path:
sys.path.insert(0, dirname)
return cls
def debug(vim, expr):
if hasattr(vim, 'out_write'):
string = (expr if isinstance(expr, str) else str(expr))
return vim.out_write('[deoplete] ' + string + '\n')
else:
vim.call('deoplete#util#print_debug', expr)
def error(vim, expr):
if hasattr(vim, 'err_write'):
string = (expr if isinstance(expr, str) else str(expr))
return vim.err_write('[deoplete] ' + string + '\n')
else:
vim.call('deoplete#util#print_error', expr)
def error_tb(vim, msg):
lines = traceback.format_exc().splitlines()
lines += ['%s. Use :messages / see above for error details.' % msg]
if hasattr(vim, 'err_write'):
vim.err_write('[deoplete] %s\n' % '\n'.join(lines))
else:
for line in lines:
vim.call('deoplete#util#print_error', line)
def error_vim(vim, msg):
throwpoint = vim.eval('v:throwpoint')
if throwpoint != '':
error(vim, 'v:throwpoint = ' + throwpoint)
exception = vim.eval('v:exception')
if exception != '':
error(vim, 'v:exception = ' + exception)
error_tb(vim, msg)
def escape(expr):
return expr.replace("'", "''")
def charpos2bytepos(encoding, input, pos):
return len(bytes(input[: pos], encoding, errors='replace'))
def bytepos2charpos(encoding, input, pos):
return len(bytes(input, encoding, errors='replace')[: pos].decode(
encoding, errors='replace'))
def get_custom(custom, source_name, key, default):
custom_source = custom['source']
if source_name not in custom_source:
return get_custom(custom, '_', key, default)
elif key in custom_source[source_name]:
return custom_source[source_name][key]
elif key in custom_source['_']:
return custom_source['_'][key]
else:
return default
def get_syn_names(vim):
return vim.call('deoplete#util#get_syn_names')
def parse_file_pattern(f, pattern):
p = re.compile(pattern)
ret = []
for l in f:
ret += p.findall(l)
return list(set(ret))
def parse_buffer_pattern(b, pattern):
return list(set(re.compile(pattern).findall('\n'.join(b))))
def fuzzy_escape(string, camelcase):
# Escape string for python regexp.
p = re.sub(r'([a-zA-Z0-9_])', r'\1.*', re.escape(string))
if camelcase and re.search(r'[A-Z]', string):
p = re.sub(r'([a-z])', (lambda pat:
'['+pat.group(1)+pat.group(1).upper()+']'), p)
p = re.sub(r'([a-zA-Z0-9_])\.\*', r'\1[^\1]*', p)
return p
def load_external_module(file, module):
current = os.path.dirname(os.path.abspath(file))
module_dir = os.path.join(os.path.dirname(current), module)
if module_dir not in sys.path:
sys.path.insert(0, module_dir)
def truncate_skipping(string, max_width, footer, footer_len):
if not string:
return ''
if len(string) <= max_width/2:
return string
if strwidth(string) <= max_width:
return string
footer += string[
-len(truncate(string[::-1], footer_len)):]
return truncate(string, max_width - strwidth(footer)) + footer
def truncate(string, max_width):
if len(string) <= max_width/2:
return string
if strwidth(string) <= max_width:
return string
width = 0
ret = ''
for c in string:
wc = charwidth(c)
if width + wc > max_width:
break
ret += c
width += wc
return ret
def strwidth(string):
width = 0
for c in string:
width += charwidth(c)
return width
def charwidth(c):
wc = unicodedata.east_asian_width(c)
return 2 if wc == 'F' or wc == 'W' else 1
def expand(path):
return os.path.expanduser(os.path.expandvars(path))
def getlines(vim, start=1, end='$'):
if end == '$':
end = len(vim.current.buffer)
max_len = min([end - start, 5000])
lines = []
current = start
while current <= end:
lines += vim.call('getline', current, current + max_len)
current += max_len + 1
return lines
def binary_search_begin(l, prefix):
if not l:
return -1
if len(l) == 1:
return 0 if l[0]['word'].lower().startswith(prefix) else -1
s = 0
e = len(l)
prefix = prefix.lower()
while s < e:
index = int((s + e) / 2)
word = l[index]['word'].lower()
if word.startswith(prefix):
if (index - 1 < 0 or not
l[index-1]['word'].lower().startswith(prefix)):
return index
e = index
elif prefix < word:
e = index
else:
s = index + 1
return -1
def binary_search_end(l, prefix):
if not l:
return -1
if len(l) == 1:
return 0 if l[0]['word'].lower().startswith(prefix) else -1
s = 0
e = len(l)
prefix = prefix.lower()
while s < e:
index = int((s + e) / 2)
word = l[index]['word'].lower()
if word.startswith(prefix):
if ((index + 1) >= len(l) or not
l[index+1]['word'].lower().startswith(prefix)):
return index
s = index + 1
elif prefix < word:
e = index
else:
s = index + 1
return -1
def uniq_list_dict(l):
# Uniq list of dictionaries
ret = []
for d in l:
if d not in ret:
ret.append(d)
return ret