summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Malcolm <dmalcolm@redhat.com>2017-05-23 12:58:19 -0400
committerDavid Malcolm <dmalcolm@redhat.com>2017-07-21 21:24:19 -0400
commit0d7e6fb75ef02b729ee347f28e6182b18031a819 (patch)
treee395af5e602a578acc49ad501ea05ebb3480d307
parentb95baa8f1b6560736fb1eb18a8c49ddac23f0ecc (diff)
downloadgcc-0d7e6fb75ef02b729ee347f28e6182b18031a819.tar.gz
Language Server Protocol: work-in-progess on testsuite
This file adds the beginnings of a testsuite for the LSP implementation. The test cases are implemented in Python; they aren't currently wired up to DejaGnu (I've been invoking them directly). There's an automated test case, and a PyGTK UI. Both require the language server to be manually started (see the comments). gcc/testsuite/ChangeLog: * gcc.dg/lsp/lsp.py: New file. * gcc.dg/lsp/test.c: New test file. * gcc.dg/lsp/test.py: New test case. * gcc.dg/lsp/toy-ide.py: New file.
-rw-r--r--gcc/testsuite/gcc.dg/lsp/lsp.py125
-rw-r--r--gcc/testsuite/gcc.dg/lsp/test.c12
-rw-r--r--gcc/testsuite/gcc.dg/lsp/test.py28
-rw-r--r--gcc/testsuite/gcc.dg/lsp/toy-ide.py111
4 files changed, 276 insertions, 0 deletions
diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.py b/gcc/testsuite/gcc.dg/lsp/lsp.py
new file mode 100644
index 00000000000..56468daf298
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/lsp.py
@@ -0,0 +1,125 @@
+# A python module for implementing LSP clients
+
+import json
+
+import jsonrpc # pip install json-rpc
+import requests
+
+# Various types to model the LSP interface
+
+class Position:
+ def __init__(self, line, character):
+ self.line = line
+ self.character = character
+
+ def __repr__(self):
+ return 'Position(%r, %r)' % (self.line, self.character)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def to_json(self):
+ return {'line': self.line, 'character': self.character}
+
+ @staticmethod
+ def from_json(js):
+ return Position(js['line'], js['character'])
+
+class Range:
+ def __init__(self, start, end):
+ self.start = start
+ self.end = end
+
+ def __repr__(self):
+ return 'Range(%r, %r)' % (self.start, self.end)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ @staticmethod
+ def from_json(js):
+ return Range(Position.from_json(js['start']),
+ Position.from_json(js['end']))
+
+class Location:
+ def __init__(self, uri, range):
+ self.uri = uri
+ self.range = range
+
+ def __repr__(self):
+ return 'Location(%r, %r)' % (self.uri, self.range)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ @staticmethod
+ def from_json(js):
+ return Location(js['uri'], Range.from_json(js['range']))
+
+ def dump(self, msg):
+ print('%s:%i:%i: %s' % (self.uri, self.range.start.line,
+ self.range.start.character, msg))
+ # FIXME: underline
+ # linecache uses 1-based line numbers, whereas LSP uses
+ # 0-based line numbers
+ import linecache
+ line = linecache.getline(self.uri, self.range.start.line + 1)
+ print('line: %r' % line)
+
+class TextDocumentIdentifier:
+ def __init__(self, uri):
+ self.uri = uri
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def to_json(self):
+ return {'uri': self.uri}
+
+class TextDocumentPositionParams:
+ def __init__(self, textDocument, position):
+ self.textDocument = textDocument
+ self.position = position
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def to_json(self):
+ return {"textDocument" : self.textDocument.to_json(),
+ "position" : self.position.to_json()}
+
+# A wrapper for making LSP calls against a server
+
+class Proxy:
+ def __init__(self, url):
+ self.url = url
+ self.next_id = 0
+
+ def make_request(self, method, params):
+ json_req = {"method": method,
+ "params": params,
+ "jsonrpc": "2.0",
+ "id": self.next_id}
+ self.next_id += 1
+ return json_req
+
+ def post_request(self, method, params):
+ payload = self.make_request(method, params)
+ headers = {'content-type': 'application/json'}
+ response = requests.post(self.url, data=json.dumps(payload),
+ headers=headers)
+ print('response: %r' % response)
+ print('response.json(): %r' % response.json())
+ return response.json()
+
+ def goto_definition(self, textDocument, position):
+ params = TextDocumentPositionParams(textDocument, position)
+ json_resp = self.post_request('textDocument/definition',
+ params.to_json())
+ print(json_resp)
+ # Expect either a Location or a list of Location
+ if isinstance(json_resp, list):
+ return [Location.from_json(item) for item in json_resp]
+ else:
+ return Location.from_json(json_resp)
+
diff --git a/gcc/testsuite/gcc.dg/lsp/test.c b/gcc/testsuite/gcc.dg/lsp/test.c
new file mode 100644
index 00000000000..bb80bd06086
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.c
@@ -0,0 +1,12 @@
+/* */
+
+struct foo
+{
+ int color;
+ int shape;
+};
+
+int test (struct foo *ptr)
+{
+ ptr->
+}
diff --git a/gcc/testsuite/gcc.dg/lsp/test.py b/gcc/testsuite/gcc.dg/lsp/test.py
new file mode 100644
index 00000000000..c7f00670079
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.py
@@ -0,0 +1,28 @@
+from lsp import Proxy, TextDocumentIdentifier, Position, Range, Location
+
+def main():
+ # This assumes that we're running this:
+ # (hardcoding the particular source file for now):
+ # ./xgcc -B. -c ../../src/gcc/testsuite/gcc.dg/lsp/test.c \
+ # -flsp=4000 -fblt -wrapper gdb,--args
+
+ url = "http://localhost:4000/jsonrpc"
+ proxy = Proxy(url)
+
+ # FIXME: filename/uri (currently a particular relative location)
+ FILENAME = '../../src/gcc/testsuite/gcc.dg/lsp/test.c'
+
+ # Ask for the location of a usage of "struct foo" (0-based lines)
+ result = proxy.goto_definition(TextDocumentIdentifier(FILENAME),
+ Position(8, 16))
+
+ # We expect to get back the location of where "struct foo" is defined
+ print(result)
+ # FIXME: filename/uri (currently a particular relative location)
+ assert result == [Location(FILENAME,
+ Range(Position(2, 0), Position(6, 1)))]
+ for loc in result:
+ loc.dump("definition")
+
+if __name__ == "__main__":
+ main()
diff --git a/gcc/testsuite/gcc.dg/lsp/toy-ide.py b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
new file mode 100644
index 00000000000..ff1d2e24157
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
@@ -0,0 +1,111 @@
+# A toy IDE implemented in PyGTK
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gtksourceview2
+#help(gtksourceview2)
+
+import lsp
+
+class ToyIde:
+ def delete_event(self, widget, event, data=None):
+ return False
+
+ def destroy(self, widget, data=None):
+ gtk.main_quit()
+
+ def quit_cb(self, b):
+ gtk.main_quit()
+
+ def get_cursor_position(self):
+ """Get the position of the cursor within the buffer
+ as an lsp.Position"""
+ mark = self.buf.get_insert()
+ print(mark)
+ iter = self.buf.get_iter_at_mark(mark)
+ print(iter)
+
+ print('line: %r' % iter.get_line()) # 0-based line
+ print('line_offset: %r' % iter.get_line_offset()) # 0-based offse
+
+ return lsp.Position(iter.get_line(), iter.get_line_offset())
+
+ def goto_definition_cb(self, b):
+ print "goto_definition_cb"
+
+ # FIXME: need to sort out paths between the LSP server and client
+ FILENAME = '../../src/gcc/testsuite/gcc.dg/lsp/test.c'
+
+ locs = self.lsp.goto_definition(lsp.TextDocumentIdentifier(FILENAME),
+ self.get_cursor_position())
+ print(locs)
+ if len(locs) == 1:
+ loc = locs[0]
+ # Both lsp.Range and gtk.TextBuffer.select_range are inclusive
+ # on the start, exclusive on the end-point
+ self.buf.select_range(
+ self.buf.get_iter_at_line_offset(loc.range.start.line,
+ loc.range.start.character),
+ self.buf.get_iter_at_line_offset(loc.range.end.line,
+ loc.range.end.character))
+
+ def __init__(self, path):
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.connect("delete_event", self.delete_event)
+ self.window.connect("destroy", self.destroy)
+ self.window.set_size_request(640, 480)
+ vbox = gtk.VBox()
+ self.window.add(vbox)
+
+ uimanager = gtk.UIManager()
+ accelgroup = uimanager.get_accel_group()
+ self.window.add_accel_group(accelgroup)
+
+ actiongroup = gtk.ActionGroup('UIManagerExample')
+ self.actiongroup = actiongroup
+ actiongroup.add_actions([('Quit', gtk.STOCK_QUIT, '_Quit', None,
+ 'Quit', self.quit_cb),
+ ('File', None, '_File'),
+ ('Test', None, '_Test'),
+ ('GotoDefinition', None, 'Goto _Definition',
+ None, 'Goto the definition of this thing',
+ self.goto_definition_cb)
+ ])
+ actiongroup.get_action('Quit').set_property('short-label', '_Quit')
+ uimanager.insert_action_group(actiongroup, 0)
+
+ merge_id = uimanager.add_ui_from_string("""
+ <ui>
+ <menubar name="MenuBar">
+ <menu action="File">
+ <menuitem action="Quit"/>
+ </menu>
+ <menu action="Test">
+ <menuitem action="GotoDefinition"/>
+ </menu>
+ </menubar>
+ </ui>
+""")
+ menubar = uimanager.get_widget('/MenuBar')
+ vbox.pack_start(menubar, False)
+
+ self.buf = gtksourceview2.Buffer()
+
+ with open(path) as f:
+ text = f.read()
+ self.buf.set_text(text)
+
+ self.sv = gtksourceview2.View(buffer=self.buf)
+ self.sv.set_show_line_numbers(True)
+
+ vbox.add(self.sv)
+ self.window.show_all()
+
+ self.lsp = lsp.Proxy('http://localhost:4000/jsonrpc')
+
+ def main(self):
+ gtk.main()
+
+ide = ToyIde("gcc/testsuite/gcc.dg/lsp/test.c")
+ide.main()