From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: (qmail 85448 invoked by alias); 24 Jul 2017 19:31:22 -0000 Mailing-List: contact gcc-patches-help@gcc.gnu.org; run by ezmlm Precedence: bulk List-Id: List-Archive: List-Post: List-Help: Sender: gcc-patches-owner@gcc.gnu.org Received: (qmail 84857 invoked by uid 89); 24 Jul 2017 19:31:21 -0000 Authentication-Results: sourceware.org; auth=none X-Virus-Found: No X-Spam-SWARE-Status: No, score=-26.9 required=5.0 tests=BAYES_00,GIT_PATCH_0,GIT_PATCH_1,GIT_PATCH_2,GIT_PATCH_3,RP_MATCHES_RCVD,SPF_HELO_PASS autolearn=ham version=3.3.2 spammy=pip, Goto, wired, Ask X-HELO: mx1.redhat.com Received: from mx1.redhat.com (HELO mx1.redhat.com) (209.132.183.28) by sourceware.org (qpsmtpd/0.93/v0.84-503-g423c35a) with ESMTP; Mon, 24 Jul 2017 19:31:17 +0000 Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com [10.5.11.16]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 03658681F3 for ; Mon, 24 Jul 2017 19:31:16 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mx1.redhat.com 03658681F3 Authentication-Results: ext-mx03.extmail.prod.ext.phx2.redhat.com; dmarc=none (p=none dis=none) header.from=redhat.com Authentication-Results: ext-mx03.extmail.prod.ext.phx2.redhat.com; spf=pass smtp.mailfrom=dmalcolm@redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 mx1.redhat.com 03658681F3 Received: from c64.redhat.com (ovpn-112-25.phx2.redhat.com [10.3.112.25]) by smtp.corp.redhat.com (Postfix) with ESMTP id 28A6069719; Mon, 24 Jul 2017 19:31:14 +0000 (UTC) From: David Malcolm To: gcc-patches@gcc.gnu.org Cc: David Malcolm Subject: [PATCH 17/17] Language Server Protocol: work-in-progess on testsuite Date: Mon, 24 Jul 2017 19:31:00 -0000 Message-Id: <1500926714-56988-18-git-send-email-dmalcolm@redhat.com> In-Reply-To: <1500926714-56988-1-git-send-email-dmalcolm@redhat.com> References: <1500926714-56988-1-git-send-email-dmalcolm@redhat.com> X-IsSubscribed: yes X-SW-Source: 2017-07/txt/msg01451.txt.bz2 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 manually). 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. --- gcc/testsuite/gcc.dg/lsp/lsp.py | 125 ++++++++++++++++++++++++++++++++++++ gcc/testsuite/gcc.dg/lsp/test.c | 12 ++++ gcc/testsuite/gcc.dg/lsp/test.py | 28 ++++++++ gcc/testsuite/gcc.dg/lsp/toy-ide.py | 111 ++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 gcc/testsuite/gcc.dg/lsp/lsp.py create mode 100644 gcc/testsuite/gcc.dg/lsp/test.c create mode 100644 gcc/testsuite/gcc.dg/lsp/test.py create mode 100644 gcc/testsuite/gcc.dg/lsp/toy-ide.py diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.py b/gcc/testsuite/gcc.dg/lsp/lsp.py new file mode 100644 index 0000000..56468da --- /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 0000000..bb80bd0 --- /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 0000000..c7f0067 --- /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 0000000..ff1d2e2 --- /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(""" + + + + + + + + + + +""") + 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() -- 1.8.5.3