forked from VimPlug/jedi
Merge branch 'feature_827_fuzzy_search' of https://github.com/jmfrank63/jedi
This commit is contained in:
@@ -201,7 +201,7 @@ class Script(object):
|
|||||||
self._inference_state.environment,
|
self._inference_state.environment,
|
||||||
)
|
)
|
||||||
|
|
||||||
def completions(self):
|
def completions(self, fuzzy=False):
|
||||||
"""
|
"""
|
||||||
Return :class:`classes.Completion` objects. Those objects contain
|
Return :class:`classes.Completion` objects. Those objects contain
|
||||||
information about the completions, more than just names.
|
information about the completions, more than just names.
|
||||||
@@ -214,7 +214,7 @@ class Script(object):
|
|||||||
self._inference_state, self._get_module_context(), self._code_lines,
|
self._inference_state, self._get_module_context(), self._code_lines,
|
||||||
self._pos, self.call_signatures
|
self._pos, self.call_signatures
|
||||||
)
|
)
|
||||||
return completion.completions()
|
return completion.completions(fuzzy)
|
||||||
|
|
||||||
def goto_definitions(self, **kwargs):
|
def goto_definitions(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def get_call_signature_param_names(call_signatures):
|
|||||||
yield p._name
|
yield p._name
|
||||||
|
|
||||||
|
|
||||||
def filter_names(inference_state, completion_names, stack, like_name):
|
def filter_names(inference_state, completion_names, stack, like_name, fuzzy):
|
||||||
comp_dct = {}
|
comp_dct = {}
|
||||||
if settings.case_insensitive_completion:
|
if settings.case_insensitive_completion:
|
||||||
like_name = like_name.lower()
|
like_name = like_name.lower()
|
||||||
@@ -36,8 +36,11 @@ def filter_names(inference_state, completion_names, stack, like_name):
|
|||||||
string = name.string_name
|
string = name.string_name
|
||||||
if settings.case_insensitive_completion:
|
if settings.case_insensitive_completion:
|
||||||
string = string.lower()
|
string = string.lower()
|
||||||
|
if fuzzy:
|
||||||
if string.startswith(like_name):
|
match = helpers.fuzzy_match(string, like_name)
|
||||||
|
else:
|
||||||
|
match = helpers.start_match(string, like_name)
|
||||||
|
if match:
|
||||||
new = classes.Completion(
|
new = classes.Completion(
|
||||||
inference_state,
|
inference_state,
|
||||||
name,
|
name,
|
||||||
@@ -70,7 +73,7 @@ def get_flow_scope_node(module_node, position):
|
|||||||
|
|
||||||
class Completion:
|
class Completion:
|
||||||
def __init__(self, inference_state, module_context, code_lines, position,
|
def __init__(self, inference_state, module_context, code_lines, position,
|
||||||
call_signatures_callback):
|
call_signatures_callback, fuzzy=False):
|
||||||
self._inference_state = inference_state
|
self._inference_state = inference_state
|
||||||
self._module_context = module_context
|
self._module_context = module_context
|
||||||
self._module_node = module_context.tree_node
|
self._module_node = module_context.tree_node
|
||||||
@@ -84,14 +87,20 @@ class Completion:
|
|||||||
self._position = position[0], position[1] - len(self._like_name)
|
self._position = position[0], position[1] - len(self._like_name)
|
||||||
self._call_signatures_callback = call_signatures_callback
|
self._call_signatures_callback = call_signatures_callback
|
||||||
|
|
||||||
def completions(self):
|
self._fuzzy = fuzzy
|
||||||
|
|
||||||
|
def completions(self, fuzzy=False, **kwargs):
|
||||||
|
return self._completions(fuzzy, **kwargs)
|
||||||
|
|
||||||
|
def _completions(self, fuzzy):
|
||||||
leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True)
|
leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True)
|
||||||
string, start_leaf = _extract_string_while_in_string(leaf, self._position)
|
string, start_leaf = _extract_string_while_in_string(leaf, self._position)
|
||||||
if string is not None:
|
if string is not None:
|
||||||
completions = list(file_name_completions(
|
completions = list(file_name_completions(
|
||||||
self._inference_state, self._module_context, start_leaf, string,
|
self._inference_state, self._module_context, start_leaf, string,
|
||||||
self._like_name, self._call_signatures_callback,
|
self._like_name, self._call_signatures_callback,
|
||||||
self._code_lines, self._original_position
|
self._code_lines, self._original_position,
|
||||||
|
fuzzy
|
||||||
))
|
))
|
||||||
if completions:
|
if completions:
|
||||||
return completions
|
return completions
|
||||||
@@ -99,7 +108,7 @@ class Completion:
|
|||||||
completion_names = self._get_value_completions(leaf)
|
completion_names = self._get_value_completions(leaf)
|
||||||
|
|
||||||
completions = filter_names(self._inference_state, completion_names,
|
completions = filter_names(self._inference_state, completion_names,
|
||||||
self.stack, self._like_name)
|
self.stack, self._like_name, fuzzy)
|
||||||
|
|
||||||
return sorted(completions, key=lambda x: (x.name.startswith('__'),
|
return sorted(completions, key=lambda x: (x.name.startswith('__'),
|
||||||
x.name.startswith('_'),
|
x.name.startswith('_'),
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import os
|
|||||||
from jedi._compatibility import FileNotFoundError, force_unicode, scandir
|
from jedi._compatibility import FileNotFoundError, force_unicode, scandir
|
||||||
from jedi.inference.names import AbstractArbitraryName
|
from jedi.inference.names import AbstractArbitraryName
|
||||||
from jedi.api import classes
|
from jedi.api import classes
|
||||||
|
from jedi.api.helpers import fuzzy_match, start_match
|
||||||
from jedi.inference.helpers import get_str_or_none
|
from jedi.inference.helpers import get_str_or_none
|
||||||
from jedi.parser_utils import get_string_quote
|
from jedi.parser_utils import get_string_quote
|
||||||
|
|
||||||
|
|
||||||
def file_name_completions(inference_state, module_context, start_leaf, string,
|
def file_name_completions(inference_state, module_context, start_leaf, string,
|
||||||
like_name, call_signatures_callback, code_lines, position):
|
like_name, call_signatures_callback, code_lines, position, fuzzy):
|
||||||
# First we want to find out what can actually be changed as a name.
|
# First we want to find out what can actually be changed as a name.
|
||||||
like_name_length = len(os.path.basename(string) + like_name)
|
like_name_length = len(os.path.basename(string) + like_name)
|
||||||
|
|
||||||
@@ -32,13 +33,17 @@ def file_name_completions(inference_state, module_context, start_leaf, string,
|
|||||||
string = to_be_added + string
|
string = to_be_added + string
|
||||||
base_path = os.path.join(inference_state.project._path, string)
|
base_path = os.path.join(inference_state.project._path, string)
|
||||||
try:
|
try:
|
||||||
listed = scandir(base_path)
|
listed = sorted(scandir(base_path), key=lambda e: e.name)
|
||||||
# OSError: [Errno 36] File name too long: '...'
|
# OSError: [Errno 36] File name too long: '...'
|
||||||
except (FileNotFoundError, OSError):
|
except (FileNotFoundError, OSError):
|
||||||
return
|
return
|
||||||
for entry in listed:
|
for entry in listed:
|
||||||
name = entry.name
|
name = entry.name
|
||||||
if name.startswith(must_start_with):
|
if fuzzy:
|
||||||
|
match = fuzzy_match(name, must_start_with)
|
||||||
|
else:
|
||||||
|
match = start_match(name, must_start_with)
|
||||||
|
if match:
|
||||||
if is_in_os_path_join or not entry.is_dir():
|
if is_in_os_path_join or not entry.is_dir():
|
||||||
if start_leaf.type == 'string':
|
if start_leaf.type == 'string':
|
||||||
quote = get_string_quote(start_leaf)
|
quote = get_string_quote(start_leaf)
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ from jedi.cache import call_signature_time_cache
|
|||||||
CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name'])
|
CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name'])
|
||||||
|
|
||||||
|
|
||||||
|
def start_match(string, like_name):
|
||||||
|
return string.startswith(like_name)
|
||||||
|
|
||||||
|
|
||||||
|
def fuzzy_match(string, like_name):
|
||||||
|
if len(like_name) <= 1:
|
||||||
|
return like_name in string
|
||||||
|
pos = string.find(like_name[0])
|
||||||
|
if pos >= 0:
|
||||||
|
return fuzzy_match(string[pos + 1:], like_name[1:])
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def sorted_definitions(defs):
|
def sorted_definitions(defs):
|
||||||
# Note: `or ''` below is required because `module_path` could be
|
# Note: `or ''` below is required because `module_path` could be
|
||||||
return sorted(defs, key=lambda x: (x.module_path or '', x.line or 0, x.column or 0, x.name))
|
return sorted(defs, key=lambda x: (x.module_path or '', x.line or 0, x.column or 0, x.name))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import jedi.utils
|
|||||||
from jedi import __version__ as __jedi_version__
|
from jedi import __version__ as __jedi_version__
|
||||||
|
|
||||||
print('REPL completion using Jedi %s' % __jedi_version__)
|
print('REPL completion using Jedi %s' % __jedi_version__)
|
||||||
jedi.utils.setup_readline()
|
jedi.utils.setup_readline(fuzzy=False)
|
||||||
|
|
||||||
del jedi
|
del jedi
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from jedi import Interpreter
|
|||||||
READLINE_DEBUG = False
|
READLINE_DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
def setup_readline(namespace_module=__main__):
|
def setup_readline(namespace_module=__main__, fuzzy=False):
|
||||||
"""
|
"""
|
||||||
Install Jedi completer to :mod:`readline`.
|
Install Jedi completer to :mod:`readline`.
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ def setup_readline(namespace_module=__main__):
|
|||||||
logging.debug("Start REPL completion: " + repr(text))
|
logging.debug("Start REPL completion: " + repr(text))
|
||||||
interpreter = Interpreter(text, [namespace_module.__dict__])
|
interpreter = Interpreter(text, [namespace_module.__dict__])
|
||||||
|
|
||||||
completions = interpreter.completions()
|
completions = interpreter.completions(fuzzy=fuzzy)
|
||||||
logging.debug("REPL completions: %s", completions)
|
logging.debug("REPL completions: %s", completions)
|
||||||
|
|
||||||
self.matches = [
|
self.matches = [
|
||||||
|
|||||||
@@ -317,3 +317,51 @@ def test_goto_follow_builtin_imports(Script):
|
|||||||
def test_docstrings_for_completions(Script):
|
def test_docstrings_for_completions(Script):
|
||||||
for c in Script('').completions():
|
for c in Script('').completions():
|
||||||
assert isinstance(c.docstring(), (str, unicode))
|
assert isinstance(c.docstring(), (str, unicode))
|
||||||
|
|
||||||
|
|
||||||
|
def test_fuzzy_completion(Script):
|
||||||
|
script = Script('string = "hello"\nstring.upper')
|
||||||
|
assert ['isupper',
|
||||||
|
'upper'] == [comp.name for comp in script.completions(fuzzy=True)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 3),
|
||||||
|
reason="requires python3.3 or higher")
|
||||||
|
def test_math_fuzzy_completion(Script):
|
||||||
|
script = Script('import math\nmath.og')
|
||||||
|
assert ['copysign', 'log', 'log10', 'log1p',
|
||||||
|
'log2'] == [comp.name for comp in script.completions(fuzzy=True)]
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 3),
|
||||||
|
reason="requires python3.3 or higher")
|
||||||
|
def test_file_fuzzy_completion(Script, tmp_path):
|
||||||
|
folder0 = tmp_path / "inference"
|
||||||
|
folder0.mkdir()
|
||||||
|
file0_path0 = folder0 / "sys_path.py"
|
||||||
|
file0_path0.write_text('\n')
|
||||||
|
file0_path1 = folder0 / "syntax_tree.py"
|
||||||
|
file0_path1.write_text('\n')
|
||||||
|
script = Script('"{}/yt'.format(folder0))
|
||||||
|
assert ['syntax_tree.py"', 'sys_path.py"'] \
|
||||||
|
== [comp.name for comp in script.completions(fuzzy=True)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info > (2, 7),
|
||||||
|
reason="requires python3.3 or higher")
|
||||||
|
def test_math_fuzzy_completion(Script):
|
||||||
|
script = Script('import math\nmath.og')
|
||||||
|
assert ['copysign', 'log', 'log10',
|
||||||
|
'log1p'] == [comp.name for comp in script.completions(fuzzy=True)]
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info > (2, 7),
|
||||||
|
reason="requires python3.3 or higher")
|
||||||
|
def test_file_fuzzy_completion(Script, tmp_path):
|
||||||
|
folder0 = tmp_path / u"inference"
|
||||||
|
folder0.mkdir()
|
||||||
|
file0_path0 = folder0 / u"sys_path.py"
|
||||||
|
file0_path0.write_text('\n')
|
||||||
|
file0_path1 = folder0 / u"syntax_tree.py"
|
||||||
|
file0_path1.write_text('\n')
|
||||||
|
script = Script('"{}/yt'.format(folder0))
|
||||||
|
assert ['syntax_tree.py"', 'sys_path.py"'] \
|
||||||
|
== [comp.name for comp in script.completions(fuzzy=True)]
|
||||||
|
|||||||
@@ -271,3 +271,15 @@ def test_file_path_completions(Script, file, code, column, expected):
|
|||||||
assert len(comps) > 100 # This is basically global completions.
|
assert len(comps) > 100 # This is basically global completions.
|
||||||
else:
|
else:
|
||||||
assert [c.complete for c in comps] == expected
|
assert [c.complete for c in comps] == expected
|
||||||
|
|
||||||
|
from jedi.api.helpers import start_match, fuzzy_match
|
||||||
|
|
||||||
|
def test_start_match():
|
||||||
|
assert start_match('Condition', 'C')
|
||||||
|
|
||||||
|
def test_fuzzy_match():
|
||||||
|
assert fuzzy_match('Condition', 'i')
|
||||||
|
assert not fuzzy_match('Condition', 'p')
|
||||||
|
assert fuzzy_match('Condition', 'ii')
|
||||||
|
assert not fuzzy_match('Condition', 'Ciito')
|
||||||
|
assert fuzzy_match('Condition', 'Cdiio')
|
||||||
|
|||||||
Reference in New Issue
Block a user