Skip to content

Commit

Permalink
Add completion types for completions obtained with matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Aug 22, 2022
1 parent 7917253 commit 17d91a8
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 15 deletions.
60 changes: 45 additions & 15 deletions IPython/core/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,17 @@ def __repr__(self):
return '<Fake completion object jedi has crashed>'


_JediCompletionLike = Union[jedi.api.Completion, _FakeJediCompletion]


def matcher(*, type: str):
"""Decorator for adding attributes to matcher-derived completion items."""
def outer(func):
func.completion_type = type
return func
return outer


class Completion:
"""
Completion object used and return by IPython completers.
Expand Down Expand Up @@ -920,6 +931,7 @@ def _safe_isinstance(obj, module, class_name):
return (module in sys.modules and
isinstance(obj, getattr(import_module(module), class_name)))

@matcher(type='unicode')
def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:
"""Match Unicode characters back to Unicode name
Expand Down Expand Up @@ -959,6 +971,8 @@ def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:
pass
return '', ()


@matcher(type='latex')
def back_latex_name_matches(text:str) -> Tuple[str, Sequence[str]] :
"""Match latex characters back to unicode name
Expand Down Expand Up @@ -1043,6 +1057,7 @@ class _CompleteResult(NamedTuple):
matches: Sequence[str]
matches_origin: Sequence[str]
jedi_matches: Any
matches_type: Sequence[Union[str, None]]


class IPCompleter(Completer):
Expand Down Expand Up @@ -1227,6 +1242,7 @@ def _clean_glob_win32(self, text:str):
return [f.replace("\\","/")
for f in self.glob("%s*" % text)]

@matcher(type='file')
def file_matches(self, text:str)->List[str]:
"""Match filenames, expanding ~USER type strings.
Expand Down Expand Up @@ -1309,6 +1325,7 @@ def file_matches(self, text:str)->List[str]:
# Mark directories in input list by appending '/' to their names.
return [x+'/' if os.path.isdir(x) else x for x in matches]

@matcher(type='magic')
def magic_matches(self, text:str):
"""Match magics"""
# Get all shell magics now rather than statically, so magics loaded at
Expand Down Expand Up @@ -1351,6 +1368,7 @@ def matches(magic):

return comp

@matcher(type='config magic')
def magic_config_matches(self, text:str) -> List[str]:
""" Match class names and attributes for %config magic """
texts = text.strip().split()
Expand Down Expand Up @@ -1386,6 +1404,7 @@ def magic_config_matches(self, text:str) -> List[str]:
if attr.startswith(texts[1]) ]
return []

@matcher(type='colors magic')
def magic_color_matches(self, text:str) -> List[str] :
""" Match color schemes for %colors magic"""
texts = text.split()
Expand All @@ -1400,9 +1419,9 @@ def magic_color_matches(self, text:str) -> List[str] :
if color.startswith(prefix) ]
return []

def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str) -> Iterable[Any]:
def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str) -> Iterable[_JediCompletionLike]:
"""
Return a list of :any:`jedi.api.Completions` object from a ``text`` and
Return a list of :any:`jedi.api.Completion`s object from a ``text`` and
cursor position.
Parameters
Expand Down Expand Up @@ -1554,6 +1573,7 @@ def _default_arguments(self, obj):

return list(set(ret))

@matcher(type='param')
def python_func_kw_matches(self, text):
"""Match named parameters (kwargs) of the last open function"""

Expand Down Expand Up @@ -1650,6 +1670,7 @@ def _get_keys(obj: Any) -> List[Any]:
return obj.dtype.names or []
return []

@matcher(type='dictionary key')
def dict_key_matches(self, text:str) -> List[str]:
"Match string keys in a dictionary, after e.g. 'foo[' "

Expand Down Expand Up @@ -1755,6 +1776,7 @@ def dict_key_matches(self, text:str) -> List[str]:
return [leading + k + suf for k in matches]

@staticmethod
@matcher(type='unicode')
def unicode_name_matches(text:str) -> Tuple[str, List[str]] :
"""Match Latex-like syntax for unicode characters base
on the name of the character.
Expand All @@ -1777,6 +1799,7 @@ def unicode_name_matches(text:str) -> Tuple[str, List[str]] :
return '', []


@matcher(type='latex')
def latex_matches(self, text:str) -> Tuple[str, Sequence[str]]:
"""Match Latex syntax for unicode characters.
Expand Down Expand Up @@ -1955,7 +1978,7 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com
before = full_text[:offset]
cursor_line, cursor_column = position_to_cursor(full_text, offset)

matched_text, matches, matches_origin, jedi_matches = self._complete(
matched_text, matches, matches_origin, jedi_matches, matches_type = self._complete(
full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column)

iter_jm = iter(jedi_matches)
Expand Down Expand Up @@ -2003,8 +2026,8 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com
# I'm unsure if this is always true, so let's assert and see if it
# crash
assert before.endswith(matched_text)
for m, t in zip(matches, matches_origin):
yield Completion(start=start_offset, end=offset, text=m, _origin=t, signature='', type='<unknown>')
for m, origin, type_ in zip(matches, matches_origin, matches_type):
yield Completion(start=start_offset, end=offset, text=m, _origin=origin, signature='', type=type_ or '<unknown>')


def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, Sequence[str]]:
Expand Down Expand Up @@ -2086,6 +2109,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
matches: list of completions ?
matches_origin: ? list same length as matches, and where each completion came from
jedi_matches: list of Jedi matches, have it's own structure.
matches_type: list same length as matches, providing completion types
"""


Expand Down Expand Up @@ -2114,8 +2138,9 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
self.fwd_unicode_match):
name_text, name_matches = meth(base_text)
if name_text:
return _CompleteResult(name_text, name_matches[:MATCHES_LIMIT], \
[meth.__qualname__]*min(len(name_matches), MATCHES_LIMIT), ())
size = min(len(name_matches), MATCHES_LIMIT)
return _CompleteResult(name_text, name_matches[:MATCHES_LIMIT],
[meth.__qualname__]*size, (), [meth.completion_type]*size)


# If no line buffer is given, assume the input text is all there was
Expand All @@ -2130,7 +2155,8 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
matches = list(matcher(line_buffer))[:MATCHES_LIMIT]
if matches:
origins = [matcher.__qualname__] * len(matches)
return _CompleteResult(text, matches, origins, ())
types = [matcher.completion_type] * len(matches)
return _CompleteResult(text, matches, origins, (), types)

# Start with a clean slate of completions
matches = []
Expand All @@ -2140,7 +2166,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
# simply collapse the dict into a list for readline, but we'd have
# richer completion semantics in other environments.
is_magic_prefix = len(text) > 0 and text[0] == "%"
completions: Iterable[Any] = []
completions: Iterable[_JediCompletionLike] = []
if self.use_jedi and not is_magic_prefix:
if not full_text:
full_text = line_buffer
Expand All @@ -2150,42 +2176,46 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
if self.merge_completions:
matches = []
for matcher in self.matchers:
completion_type = matcher.completion_type if hasattr(matcher, 'completion_type') else None
try:
matches.extend([(m, matcher.__qualname__)
matches.extend([(m, matcher.__qualname__, completion_type)
for m in matcher(text)])
except:
# Show the ugly traceback if the matcher causes an
# exception, but do NOT crash the kernel!
sys.excepthook(*sys.exc_info())
else:
for matcher in self.matchers:
matches = [(m, matcher.__qualname__)
completion_type = matcher.completion_type if hasattr(matcher, 'completion_type') else None
matches = [(m, matcher.__qualname__, completion_type)
for m in matcher(text)]
if matches:
break

seen = set()
filtered_matches = set()
for m in matches:
t, c = m
t, *_ = m
if t not in seen:
filtered_matches.add(m)
seen.add(t)

_filtered_matches = sorted(filtered_matches, key=lambda x: completions_sorting_key(x[0]))

custom_res = [(m, 'custom') for m in self.dispatch_custom_completer(text) or []]
custom_res = [(m, 'custom', None) for m in self.dispatch_custom_completer(text) or []]

_filtered_matches = custom_res or _filtered_matches

_filtered_matches = _filtered_matches[:MATCHES_LIMIT]
_matches = [m[0] for m in _filtered_matches]
origins = [m[1] for m in _filtered_matches]
types = [m[2] for m in _filtered_matches]

self.matches = _matches

return _CompleteResult(text, _matches, origins, completions)

return _CompleteResult(text, _matches, origins, completions, types)

@matcher(type='unicode')
def fwd_unicode_match(self, text:str) -> Tuple[str, Sequence[str]]:
"""
Forward match a string starting with a backslash with a list of
Expand Down
11 changes: 11 additions & 0 deletions IPython/core/tests/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,17 @@ def test_completion_have_signature(self):
"encoding" in c.signature
), "Signature of function was not found by completer"

def test_completions_have_type(self):
"""
Lets make sure matchers provide completion type.
"""
ip = get_ipython()
with provisionalcompleter():
ip.Completer.use_jedi = False
completions = ip.Completer.completions("%tim", 3)
c = next(completions) # should be `%time` or similar
assert c.type == 'magic', "Signature of function was not found by completer"

@pytest.mark.xfail(reason="Known failure on jedi<=0.18.0")
def test_deduplicate_completions(self):
"""
Expand Down

0 comments on commit 17d91a8

Please sign in to comment.