diff --git a/julia/magic.py b/julia/magic.py index 96b3d7ae..f9e43160 100644 --- a/julia/magic.py +++ b/julia/magic.py @@ -66,6 +66,74 @@ def julia(self, line, cell=None): return ans +class JuliaCompleter(object): + + """ + Simple completion for ``%julia`` line magic. + """ + + @property + def jlcomplete_texts(self): + try: + return self._jlcomplete_texts + except AttributeError: + pass + + julia = Julia() + if julia.eval('VERSION < v"0.7-"'): + self._jlcomplete_texts = lambda *_: [] + return self._jlcomplete_texts + + self._jlcomplete_texts = julia.eval(""" + import REPL + (str, pos) -> begin + ret, _, should_complete = + REPL.completions(str, pos) + if should_complete + return map(REPL.completion_text, ret) + else + return [] + end + end + """) + return self._jlcomplete_texts + + def complete_command(self, _ip, event): + pos = event.text_until_cursor.find("%julia") + if pos < 0: + return [] + pos += len("%julia") # pos: beginning of Julia code + julia_code = event.line[pos:] + julia_pos = len(event.text_until_cursor) - pos + + completions = self.jlcomplete_texts(julia_code, julia_pos) + if "." in event.symbol: + # When completing (say) "Base.s" we need to add the prefix "Base." + prefix = event.symbol.rsplit(".", 1)[0] + completions = [".".join((prefix, c)) for c in completions] + return completions + # See: + # IPython.core.completer.dispatch_custom_completer + + @classmethod + def register(cls, ip): + """ + Register `.complete_command` to IPython hook. + + Parameters + ---------- + ip : IPython.InteractiveShell + IPython `.InteractiveShell` instance passed to + `load_ipython_extension`. + """ + ip.set_hook("complete_command", cls().complete_command, + str_key="%julia") +# See: +# https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.hooks.html +# IPython.core.interactiveshell.init_completer +# IPython.core.completerlib (quick_completer etc.) + + # Add to the global docstring the class information. __doc__ = __doc__.format( JULIAMAGICS_DOC=' ' * 8 + JuliaMagics.__doc__, @@ -80,3 +148,4 @@ def julia(self, line, cell=None): def load_ipython_extension(ip): """Load the extension in IPython.""" ip.register_magics(JuliaMagics) + JuliaCompleter.register(ip) diff --git a/test/test_magic.py b/test/test_magic.py index 70344d4c..f291a26a 100644 --- a/test/test_magic.py +++ b/test/test_magic.py @@ -1,6 +1,20 @@ from IPython.testing.globalipapp import get_ipython +import pytest + +from julia.core import Julia +from julia.magic import JuliaCompleter import julia.magic +try: + from types import SimpleNamespace +except ImportError: + from argparse import Namespace as SimpleNamespace # Python 2 + +try: + string_types = (unicode, str) +except NameError: + string_types = (str,) + def get_julia_magics(): return julia.magic.JuliaMagics(shell=get_ipython()) @@ -32,3 +46,50 @@ def test_failure_cell(): jm = get_julia_magics() ans = jm.julia(None, '1 += 1') assert ans is None + + +def make_event(line, text_until_cursor=None, symbol=""): + if text_until_cursor is None: + text_until_cursor = line + return SimpleNamespace( + line=line, + text_until_cursor=text_until_cursor, + symbol=symbol, + ) + + +completable_events = [ + make_event("%julia "), + make_event("%julia si"), + make_event("%julia Base.si"), +] + +uncompletable_events = [ + make_event(""), + make_event("%julia si", text_until_cursor="%ju"), +] + + +def check_version(): + julia = Julia() + if julia.eval('VERSION < v"0.7-"'): + raise pytest.skip("Completion not supported in Julia 0.6") + + +@pytest.mark.parametrize("event", completable_events) +def test_completable_events(event): + jc = JuliaCompleter() + dummy_ipython = None + completions = jc.complete_command(dummy_ipython, event) + assert isinstance(completions, list) + check_version() + assert set(map(type, completions)) <= set(string_types) + + +@pytest.mark.parametrize("event", uncompletable_events) +def test_uncompletable_events(event): + jc = JuliaCompleter() + dummy_ipython = None + completions = jc.complete_command(dummy_ipython, event) + assert isinstance(completions, list) + assert not completions