[gnome-builder] python: add very basic auto-completion for python using jedi



commit 1429e043ba9df3ec817d0bc20b6d4857f9878cd3
Author: Christian Hergert <christian hergert me>
Date:   Wed Jul 15 12:52:39 2015 -0700

    python: add very basic auto-completion for python using jedi
    
    Long term, this really should be put in gnome-code-assistance, but we
    still need to figure out the right abstraction there. Creating
    abstractions before we understand what is needed is generally a bad idea,
    so lets prototype this in process.
    
    Igor Gnatenko mentioned that Jedi is a pretty good completion library for
    Python. Thankfully, it seems to be packaged on a few systems including
    Fedora.
    
    If jedi is not found, this plugin will simply do nothing. On Fedora
    systems, "dnf install python3-jedi" should get the job done.
    
    Currently, this performs the lookup using Python's big Thread. I don't
    like this because it means we get all the python memory fragmentation
    with it. This is another great reason for this to belong in
    gnome-code-assistance. Multiprocessing is another short term stop gap
    that could be used. The threading/GIL shouldn't affect us much here,
    but with CPython, always famous last words.
    
    If you know python, I'm sure you can improve this module.
    
    Patches wanted, accepted.

 configure.ac                |    1 +
 plugins/Makefile.am         |    1 +
 plugins/jedi/Makefile.am    |   10 +++
 plugins/jedi/jedi.plugin    |   10 +++
 plugins/jedi/jedi_plugin.py |  186 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 208 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 176c124..12c9042 100644
--- a/configure.ac
+++ b/configure.ac
@@ -392,6 +392,7 @@ AC_CONFIG_FILES([
        plugins/file-search/Makefile
        plugins/gnome-code-assistance/Makefile
        plugins/html-completion/Makefile
+       plugins/jedi/Makefile
        plugins/mingw/Makefile
        plugins/python-pack/Makefile
        plugins/symbol-tree/Makefile
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 276090f..bf85fba 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -8,6 +8,7 @@ SUBDIRS = \
        file-search \
        gnome-code-assistance \
        html-completion \
+       jedi \
        mingw \
        python-pack \
        symbol-tree \
diff --git a/plugins/jedi/Makefile.am b/plugins/jedi/Makefile.am
new file mode 100644
index 0000000..1b71bfa
--- /dev/null
+++ b/plugins/jedi/Makefile.am
@@ -0,0 +1,10 @@
+EXTRA_DIST = $(plugin_DATA)
+
+plugindir = $(libdir)/gnome-builder/plugins
+plugin_DATA = \
+       jedi.plugin \
+       jedi_plugin.py
+
+GITIGNOREFILES = __pycache__
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/jedi/jedi.plugin b/plugins/jedi/jedi.plugin
new file mode 100644
index 0000000..8a0b7d9
--- /dev/null
+++ b/plugins/jedi/jedi.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Module=jedi_plugin
+Loader=python3
+Name=Jedi Auto-Completion
+Description=Provides autocompletion features for the Python programming language.
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2015 Christian Hergert
+Builtin=true
+Hidden=true
+X-Completion-Provider-Languages=python,python3
diff --git a/plugins/jedi/jedi_plugin.py b/plugins/jedi/jedi_plugin.py
new file mode 100644
index 0000000..a047bba
--- /dev/null
+++ b/plugins/jedi/jedi_plugin.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+
+#
+# jedi_plugin.py
+#
+# Copyright (C) 2015 Christian Hergert <chris dronelabs com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import GtkSource
+from gi.repository import Ide
+
+try:
+    import jedi
+    HAS_JEDI=True
+except ImportError:
+    HAS_JEDI=False
+
+# FIXME: Should we be using multiprocessing or something?
+#        Alternatively, this can go in gnome-code-assistance
+#        once we have an API that can transfer completions
+#        relatively fast enough for interactivity.
+import threading
+
+class CompletionThread(threading.Thread):
+    def __init__(self, provider, context, text, line, column, filename):
+        super().__init__()
+        self._provider = provider
+        self._context = context
+        self._text = text
+        self._line = line
+        self._column = column
+        self._filename = filename
+        self._completions = []
+
+    def run(self):
+        try:
+            script = jedi.Script(self._text, self._line, self._column, self._filename)
+            self._completions = [JediCompletionProposal(self._provider, self._context, info)
+                                 for info in script.completions()]
+        finally:
+            self.complete_in_idle()
+
+    def _complete(self):
+        self._context.add_proposals(self._provider, self._completions, True)
+
+    def complete_in_idle(self):
+        GLib.timeout_add(0, lambda *_: self._complete())
+
+class JediCompletionProvider(Ide.Object,
+                             GtkSource.CompletionProvider,
+                             Ide.CompletionProvider):
+    def do_get_name(self):
+        return 'Jedi Provider'
+
+    def do_get_icon(self):
+        return None
+
+    def do_populate(self, context):
+        if not HAS_JEDI:
+            context.add_proposals(self, [], True)
+            return
+
+        _, iter = context.get_iter()
+        buffer = iter.get_buffer()
+        begin, end = buffer.get_bounds()
+
+        filename = (iter.get_buffer()
+                        .get_file()
+                        .get_file()
+                        .get_basename())
+
+        text = buffer.get_text(begin, end, True)
+        line = iter.get_line() + 1
+        column = iter.get_line_offset()
+
+        CompletionThread(self, context, text, line, column, filename).start()
+
+    def do_get_activiation(self):
+        return GtkSource.CompletionActivation.INTERACTIVE
+
+    def do_match(self, context):
+        self.do_activate_proposal
+        return HAS_JEDI
+
+    def do_get_info_widget(self, proposal):
+        return None
+
+    def do_update_info(self, proposal, info):
+        pass
+
+    def do_get_start_iter(self, context, proposal):
+        _, iter = context.get_iter()
+        return True, iter
+
+    def do_activate_proposal(self, provider, proposal):
+        return False, None
+
+    def do_get_interactive_delay(self):
+        return -1
+
+    def do_get_priority(self):
+        return 200
+
+class JediCompletionProposal(GObject.Object, GtkSource.CompletionProposal):
+    def __init__(self, provider, context, completion, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.provider = provider
+        self.context = context
+        self.completion = completion
+
+    def do_get_label(self):
+        return self.completion.name
+
+    def do_get_markup(self):
+        return self.completion.name
+
+    def do_get_text(self):
+        return self.completion.complete
+
+    def do_get_icon(self):
+        if self.completion.type == 'class':
+            return load_icon(self.context, 'lang-class-symbolic')
+        elif self.completion.type == 'instance':
+            return load_icon(self.context, 'lang-variable-symbolic')
+        elif self.completion.type in ('import', 'module'):
+            # FIXME: Would be nice to do something better here.
+            return load_icon(self.context, 'lang-include-symbolic')
+        elif self.completion.type == 'function':
+            return load_icon(self.context, 'lang-function-symbolic')
+        elif self.completion.type == 'keyword':
+            # FIXME: And here
+            return None
+        return None
+
+    def do_hash(self):
+        return hash(self.completion.full_name)
+
+    def do_equal(self, other):
+        return False
+
+    def do_changed(self):
+        pass
+
+_icon_cache = {}
+
+def purge_cache():
+    _icon_cache.clear()
+
+settings = Gtk.Settings.get_default()
+settings.connect('notify::gtk-theme-name', lambda *_: purge_cache())
+settings.connect('notify::gtk-application-prefer-dark-theme', lambda *_: purge_cache())
+
+def load_icon(context, name):
+    if name in _icon_cache:
+        return _icon_cache[name]
+
+    window = context.props.completion.get_info_window()
+    size = 16 * window.get_scale_factor()
+    style_context = window.get_style_context()
+    icon_theme = Gtk.IconTheme.get_default()
+    icon_info = icon_theme.lookup_icon(name, size, 0)
+    if not icon_info:
+        icon = None
+    else:
+        icon = icon_info.load_symbolic_for_context(style_context)
+
+    _icon_cache[name] = icon
+
+    return icon
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]