[gnome-builder/wip/tingping/gjs-symbols] Create GJS SymbolProvider plugin



commit ecb6be71fe55353060dd7fedeac62025f1daafc1
Author: Patrick Griffis <tingping tingping se>
Date:   Mon Sep 11 19:09:01 2017 -0400

    Create GJS SymbolProvider plugin

 meson_options.txt                      |    1 +
 plugins/gjs-symbols/gjs_symbols.plugin |   10 ++
 plugins/gjs-symbols/gjs_symbols.py     |  248 ++++++++++++++++++++++++++++++++
 plugins/gjs-symbols/meson.build        |   13 ++
 plugins/meson.build                    |    2 +
 5 files changed, 274 insertions(+), 0 deletions(-)
---
diff --git a/meson_options.txt b/meson_options.txt
index 6cf20b6..e2dcacc 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -45,6 +45,7 @@ option('with_gcc', type: 'boolean')
 option('with_gdb', type: 'boolean')
 option('with_gettext', type: 'boolean')
 option('with_git', type: 'boolean')
+option('with_gjs_symbols', type: 'boolean')
 option('with_gnome_code_assistance', type: 'boolean')
 option('with_history', type: 'boolean')
 option('with_html_completion', type: 'boolean')
diff --git a/plugins/gjs-symbols/gjs_symbols.plugin b/plugins/gjs-symbols/gjs_symbols.plugin
new file mode 100644
index 0000000..e014098
--- /dev/null
+++ b/plugins/gjs-symbols/gjs_symbols.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Module=gjs_symbols
+Loader=python3
+Name=GJS Symbol Resolver
+Description=Provides a symbol resolver for JavaScript using GJS.
+Authors=Patrick Griffis <tingping tingping se>
+Copyright=Copyright © 2017 Patrick Griffis
+Builtin=true
+X-Symbol-Resolver-Languages=js
+X-Symbol-Resolver-Languages-Priority=0
diff --git a/plugins/gjs-symbols/gjs_symbols.py b/plugins/gjs-symbols/gjs_symbols.py
new file mode 100644
index 0000000..e631883
--- /dev/null
+++ b/plugins/gjs-symbols/gjs_symbols.py
@@ -0,0 +1,248 @@
+import gi
+import json
+import threading
+
+gi.require_versions({
+    'Ide': '1.0',
+})
+
+from gi.repository import (
+    GLib,
+    GObject,
+    Gio,
+    Ide,
+)
+
+
+SYMBOL_PARAM_FLAGS=flags = GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.READWRITE
+
+
+class JsSymbolNode(Ide.SymbolNode):
+    file = GObject.Property(type=Ide.File, flags=SYMBOL_PARAM_FLAGS)
+    line = GObject.Property(type=int, flags=SYMBOL_PARAM_FLAGS)
+    col = GObject.Property(type=int, flags=SYMBOL_PARAM_FLAGS)
+
+    def __init__(self, children, **kwargs):
+        super().__init__(**kwargs)
+        assert self.file is not None
+        self.children = children
+
+    def do_get_location_async(self, cancellable, callback, user_data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.return_boolean(True)
+
+    def do_get_location_finish(self, result):
+        if result.propagate_boolean():
+            return Ide.SourceLocation.new(self.file, self.line, self.col, 0)
+
+    def __len__(self):
+        return len(self.children)
+
+    def __getitem__(self, key):
+        return self.children[key]
+
+    def __iter__(self):
+        return self.children
+
+    def __repr__(self):
+        return '<JsSymbolNode {} ({})'.format(self.props.name, self.props.kind)
+
+
+class JsSymbolTree(GObject.Object, Ide.SymbolTree):
+    def __init__(self, dict_, file_):
+        super().__init__()
+        # with open('dump.json', 'w+') as f:
+        #    f.write(json.dumps(dict_, indent=3))
+        self.root_node = self._node_from_dict(dict_, file_)
+
+    # For now lets try to extract only a small number of useful symbols:
+    # - global properties, functions, classes, and gobject classes
+    # - methods to those classes
+    # This will be expanded upon as time goes on.
+    @staticmethod
+    def _node_from_dict(dict_, file_):
+        line = max(dict_['loc']['start']['line'] - 1, 0)
+        col = dict_['loc']['start']['column']
+
+        # FIXME: Recursion is bad in Python, I know..
+        type_ = dict_['type']
+        if type_ == 'Program':
+            children = JsSymbolTree._nodes_from_list(dict_['body'], file_)
+            return JsSymbolNode(children, line=line, col=col,
+                                kind=Ide.SymbolKind.PACKAGE,
+                                name=dict_['loc']['source'],
+                                file=file_)
+        elif type_ == 'FunctionDeclaration':
+            return JsSymbolNode([], line=line, col=col,
+                                kind=Ide.SymbolKind.FUNCTION,
+                                name=dict_['id']['name'],
+                                file=file_)
+        elif type_ == 'VariableDeclaration':
+            decs = []
+            for dec in dict_['declarations']:
+                line = max(dec['id']['loc']['start']['line'] - 1, 0)
+                col = dec['id']['loc']['start']['column']
+                name = dec['id']['name']
+                kind = Ide.SymbolKind.VARIABLE
+                children = []
+
+                if JsSymbolTree._is_module_import(dec):
+                    kind = Ide.SymbolKind.MODULE
+                elif JsSymbolTree._is_gobject_class(dec):
+                    for arg in dec['init']['arguments']:
+                        if arg['type'] == 'ClassExpression':
+                            line = max(arg['id']['loc']['start']['line'] - 1, 0)
+                            col = arg['id']['loc']['start']['column']
+                            kind = Ide.SymbolKind.CLASS
+                            children = JsSymbolTree._nodes_from_list(arg['body'], file_)
+                            name = arg['id']['name']
+                            break
+                elif dict_.get('kind', None) == 'const':
+                    kind = Ide.SymbolKind.CONSTANT
+                decs.append(JsSymbolNode(children, line=line, col=col,
+                                         kind=kind, name=name, file=file_))
+            return decs
+        elif type_ == 'ClassStatement':
+            children = JsSymbolTree._nodes_from_list(dict_['body'], file_)
+            return JsSymbolNode(children, line=line, col=col,
+                                kind=Ide.SymbolKind.CLASS,
+                                name=dict_['id']['name'],
+                                file=file_)
+        elif type_ == 'ClassMethod':
+            name = dict_['name']['name']
+            if name == 'constructed':
+                return None
+            return JsSymbolNode([], line=line, col=col,
+                                kind=Ide.SymbolKind.METHOD,
+                                name=name,
+                                file=file_)
+        else:
+            return None
+
+    @staticmethod
+    def _is_module_import(dict_):
+        try:
+            return dict_['init']['object']['name'] == 'imports'
+        except KeyError:
+            return False
+
+    @staticmethod
+    def _is_gobject_class(dict_):
+        try:
+            callee = dict_['init']['callee']
+            return callee['object']['name'].lower() == 'gobject' and callee['property']['name'] == 
'registerClass'
+        except KeyError:
+            return False
+
+    def _nodes_from_list(list_, file_):
+        nodes = []
+        for i in list_:
+            node = JsSymbolTree._node_from_dict(i, file_)
+            if node is not None:
+                if isinstance(node, list):
+                    nodes += node
+                else:
+                    nodes.append(node)
+        return nodes
+
+    def do_get_n_children(self, node):
+        return len(node) if node is not None else len(self.root_node)
+
+    def do_get_nth_child(self, node, nth):
+        return node[nth] if node is not None else self.root_node[nth]
+
+
+JS_SCRIPT = \
+"""var data;
+if (ARGV[0] === '--file') {
+  const GLib = imports.gi.GLib;
+  var ret = GLib.file_get_contents(ARGV[1]);
+  data = ret[1];
+} else {
+  data = ARGV[0];
+}
+print(JSON.stringify(Reflect.parse(data, {source: '%s'})));""".replace('\n', ' ')
+
+
+class GjsSymbolProvider(Ide.Object, Ide.SymbolResolver):
+    def __init__(self):
+        super().__init__()
+
+    def _get_launcher(self, file_):
+        context = self.get_context()
+
+        file_path = file_.get_path()
+        script = JS_SCRIPT %file_path
+        unsaved_file = context.get_unsaved_files().get_unsaved_file(file_)
+
+        pipeline = context.get_build_manager().get_pipeline()
+        runtime = pipeline.get_configuration().get_runtime()
+
+        launcher = runtime.create_launcher()
+        launcher.set_flags(Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE)
+        launcher.push_args(('gjs', '-c', script))
+        if unsaved_file is not None:
+            launcher.push_argv(unsaved_file.get_content().get_data().decode('utf-8'))
+        else:
+            launcher.push_args(('--file', file_path))
+        return launcher
+
+    def do_lookup_symbol_async(self, location, cancellable, callback, user_data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.return_boolean(False)  # Not implemented
+
+    def do_lookup_symbol_finish(self, result):
+        result.propagate_boolean()
+        return None
+
+    def do_get_symbol_tree_async(self, file_, buffer_, cancellable, callback, user_data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        launcher = self._get_launcher(file_)
+
+        threading.Thread(target=self._get_tree_thread, args=(task, launcher, file_),
+                         name='gjs-symbols-thread').start()
+
+    def _get_tree_thread(self, task, launcher, file_):
+        try:
+            proc = launcher.spawn()
+            success, stdout, stderr = proc.communicate_utf8(None, None)
+
+            if not success:
+                task.return_boolean(False)
+                return
+
+            ide_file = Ide.File(file=file_, context=self.get_context())
+            task.symbol_tree = JsSymbolTree(json.loads(stdout), ide_file)
+        except GLib.Error as err:
+            task.return_error(err)
+        except (json.JSONDecodeError, UnicodeDecodeError) as e:
+            task.return_error(GLib.Error('Failed to decode gjs json: {}'.format(e)))
+        except (IndexError, KeyError) as e:
+            task.return_error(GLib.Error('Failed to extract information from ast: {}'.format(e)))
+        else:
+            task.return_boolean(True)
+
+    def do_get_symbol_tree_finish(self, result):
+        if result.propagate_boolean():
+            return result.symbol_tree
+
+    def do_load(self):
+        pass
+
+    def do_find_references_async(self, location, cancellable, callback, user_data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.return_boolean(False)  # Not implemented
+
+    def do_find_references_finish(self, result):
+        result.propagate_boolean()
+        return None
+
+    def do_find_nearest_scope_async(self, location, cancellable, callback, user_data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.return_boolean(False)  # Not implemented
+
+    def do_find_nearest_scope_finish(self, result):
+        result.propagate_boolean()
+        return None
+
+
diff --git a/plugins/gjs-symbols/meson.build b/plugins/gjs-symbols/meson.build
new file mode 100644
index 0000000..9a8759a
--- /dev/null
+++ b/plugins/gjs-symbols/meson.build
@@ -0,0 +1,13 @@
+if get_option('with_gjs_symbols')
+
+install_data('gjs_symbols.py', install_dir: plugindir)
+
+configure_file(
+          input: 'gjs_symbols.plugin',
+         output: 'gjs_symbols.plugin',
+  configuration: configuration_data(),
+        install: true,
+    install_dir: plugindir,
+)
+
+endif
diff --git a/plugins/meson.build b/plugins/meson.build
index 320fb02..54da7e1 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -34,6 +34,7 @@ subdir('gcc')
 subdir('gdb')
 subdir('gettext')
 subdir('git')
+subdir('gjs-symbols')
 subdir('gnome-code-assistance')
 subdir('history')
 subdir('html-completion')
@@ -90,6 +91,7 @@ status += [
   'GDB ................... : @0@'.format(get_option('with_gdb')),
   'Gettext ............... : @0@'.format(get_option('with_gettext')),
   'Git ................... : @0@'.format(get_option('with_git')),
+  'GJS Symbol Resolver ... : @0@'.format(get_option('with_gjs_symbols')),
   'GNOME Code Assistance . : @0@'.format(get_option('with_gnome_code_assistance')),
   'History ............... : @0@'.format(get_option('with_history')),
   'HTML Completion ....... : @0@'.format(get_option('with_html_completion')),


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