diff --git a/examples/clangd.py b/examples/clangd.py index d743193..922c7e8 100644 --- a/examples/clangd.py +++ b/examples/clangd.py @@ -1,7 +1,7 @@ import pylspclient import subprocess import threading - +import argparse class ReadPipe(threading.Thread): def __init__(self, pipe): @@ -15,8 +15,11 @@ class ReadPipe(threading.Thread): line = self.pipe.readline().decode('utf-8') if __name__ == "__main__": - clangd_path = "/usr/bin/clangd-6.0" - p = subprocess.Popen(clangd_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + parser = argparse.ArgumentParser(description='pylspclient example with clangd') + parser.add_argument('clangd_path', type=str, default="/usr/bin/clangd-6.0", + help='the clangd path', nargs="?") + args = parser.parse_args() + p = subprocess.Popen([args.clangd_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) read_pipe = ReadPipe(p.stderr) read_pipe.start() json_rpc_endpoint = pylspclient.JsonRpcEndpoint(p.stdin, p.stdout) @@ -136,7 +139,7 @@ if __name__ == "__main__": 25, 26]}},'workspaceEdit': {'documentChanges': True}, 'workspaceFolders': True}} - root_uri = 'file:///home/osboxes/projects/ctest' + root_uri = 'file:///home/osboxes/projects/ctest/' workspace_folders = [{'name': 'python-lsp', 'uri': root_uri}] print(lsp_client.initialize(p.pid, None, root_uri, None, capabilities, "off", workspace_folders)) print(lsp_client.initialized()) @@ -147,11 +150,16 @@ if __name__ == "__main__": languageId = pylspclient.lsp_structs.LANGUAGE_IDENTIFIER.C version = 1 lsp_client.didOpen(pylspclient.lsp_structs.TextDocumentItem(uri, languageId, version, text)) - # documentSymbol is supported from version 8. - #lsp_client.documentSymbol(pylspclient.lsp_structs.TextDocumentIdentifier(uri)) + try: + symbols = lsp_client.documentSymbol(pylspclient.lsp_structs.TextDocumentIdentifier(uri)) + for symbol in symbols: + print(symbol.name) + except pylspclient.lsp_structs.ResponseError: + # documentSymbol is supported from version 8. + print("Failed to document symbols") - lsp_client.definition(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(15, 4)) - lsp_client.signatureHelp(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(15, 4)) - lsp_client.completion(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(15, 4), pylspclient.lsp_structs.CompletionContext(pylspclient.lsp_structs.CompletionTriggerKind.Invoked)) + lsp_client.definition(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4)) + lsp_client.signatureHelp(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4)) + lsp_client.completion(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4), pylspclient.lsp_structs.CompletionContext(pylspclient.lsp_structs.CompletionTriggerKind.Invoked)) lsp_client.shutdown() lsp_client.exit() diff --git a/pylspclient/json_rpc_endpoint.py b/pylspclient/json_rpc_endpoint.py index 85e4bbd..3b1c040 100644 --- a/pylspclient/json_rpc_endpoint.py +++ b/pylspclient/json_rpc_endpoint.py @@ -5,15 +5,18 @@ from pylspclient import lsp_structs import threading JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}" -JSON_RPC_RES_REGEX = "Content-Length: ([0-9]*)\r\n" +LEN_HEADER = "Content-Length: " +TYPE_HEADER = "Content-Type: " + + # TODO: add content-type -class MyEncoder(json.JSONEncoder): +class MyEncoder(json.JSONEncoder): """ Encodes an object in JSON """ - def default(self, o): + def default(self, o): # pylint: disable=E0202 return o.__dict__ @@ -59,20 +62,33 @@ class JsonRpcEndpoint(object): :return: a message ''' with self.read_lock: - line = self.stdout.readline() - if not line: - return None - line = line.decode("utf-8") - # TODO: handle content type as well. - match = re.match(JSON_RPC_RES_REGEX, line) - if match is None or not match.groups(): - raise RuntimeError("Bad header: " + line) - size = int(match.groups()[0]) - line = self.stdout.readline() - if not line: - return None - line = line.decode("utf-8") - if line != "\r\n": - raise RuntimeError("Bad header: missing newline") - jsonrpc_res = self.stdout.read(size).decode("utf-8") + message_size = None + while True: + #read header + line = self.stdout.readline() + if not line: + # server quit + return None + line = line.decode("utf-8") + if not line.endswith("\r\n"): + raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: missing newline") + #remove the "\r\n" + line = line[:-2] + if line == "": + # done with the headers + break + elif line.startswith(LEN_HEADER): + line = line[len(LEN_HEADER):] + if not line.isdigit(): + raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: size is not int") + message_size = int(line) + elif line.startswith(TYPE_HEADER): + # nothing todo with type for now. + pass + else: + raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: unkown header") + if not message_size: + raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.ParseError, "Bad header: missing size") + + jsonrpc_res = self.stdout.read(message_size).decode("utf-8") return json.loads(jsonrpc_res) diff --git a/pylspclient/lsp_endpoint.py b/pylspclient/lsp_endpoint.py index ffda502..48f2df4 100644 --- a/pylspclient/lsp_endpoint.py +++ b/pylspclient/lsp_endpoint.py @@ -46,15 +46,16 @@ class LspEndpoint(threading.Thread): if rpc_id: # a call for method if method not in self.method_callbacks: - raise lsp_structs.ResponseError("Method not found: {method}".format(method=method), lsp_structs.ErrorCodes.MethodNotFound) + raise lsp_structs.ResponseError(lsp_structs.ErrorCodes.MethodNotFound, "Method not found: {method}".format(method=method)) result = self.method_callbacks[method](params) self.send_response(rpc_id, result, None) else: # a call for notify if method not in self.notify_callbacks: - raise lsp_structs.ResponseError("Method not found: {method}".format(method=method), lsp_structs.ErrorCodes.MethodNotFound) - - self.notify_callbacks[method](params) + # Have nothing to do with this. + print("Notify method not found: {method}.".format(method=method)) + else: + self.notify_callbacks[method](params) else: self.handle_result(rpc_id, result, error) except lsp_structs.ResponseError as e: diff --git a/pylspclient/lsp_structs.py b/pylspclient/lsp_structs.py index 04ebaa3..6e53448 100644 --- a/pylspclient/lsp_structs.py +++ b/pylspclient/lsp_structs.py @@ -1,3 +1,6 @@ +import enum + + def to_type(o, new_type): ''' Helper funciton that receives an object or a dict and convert it to a new given type. @@ -149,7 +152,7 @@ class TextDocumentPositionParams(object): self.position = position -class LANGUAGE_IDENTIFIER: +class LANGUAGE_IDENTIFIER(object): BAT="bat" BIBTEX="bibtex" CLOJURE="clojure" @@ -201,7 +204,7 @@ class LANGUAGE_IDENTIFIER: YAML="yaml" -class SymbolKind(object): +class SymbolKind(enum.Enum): File = 1 Module = 2 Namespace = 3 @@ -256,7 +259,7 @@ class SymbolInformation(object): :param bool deprecated: Indicates if this symbol is deprecated. """ self.name = name - self.kind = kind + self.kind = SymbolKind(kind) self.deprecated = deprecated self.location = to_type(location, Location) self.containerName = containerName @@ -426,7 +429,7 @@ class CompletionList(object): self.isIncomplete = isIncomplete self.items = [to_type(i, CompletionItem) for i in items] -class ErrorCodes(object): +class ErrorCodes(enum.Enum): # Defined by JSON RPC ParseError = -32700 InvalidRequest = -32600 @@ -447,4 +450,4 @@ class ResponseError(Exception): self.code = code self.message = message if data: - self.data = data \ No newline at end of file + self.data = data diff --git a/setup.py b/setup.py index 63a6db6..edbd494 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,10 @@ class PyTest(TestCommand): errno = pytest.main(self.pytest_args) sys.exit(errno) + +install_requires=["enum34;python_version<'3.4'"] +tests_require=["pytest", "pytest_mock"] + install_requires + setup( name="pylspclient", version="0.0.2", @@ -30,6 +34,7 @@ setup( long_description_content_type="text/markdown", url="https://github.com/yeger00/pylspclient", packages=find_packages(), - tests_require=["pytest", "pytest_mock"], + install_requires=install_requires, + tests_require=tests_require, cmdclass={"test": PyTest}, ) diff --git a/tests/test_json_rpc_endpoint.py b/tests/test_json_rpc_endpoint.py index 39b4642..4999c4d 100644 --- a/tests/test_json_rpc_endpoint.py +++ b/tests/test_json_rpc_endpoint.py @@ -1,29 +1,13 @@ import os -#from pytest_mock import mocker -import pytest import pylspclient - - -class StdinMock(object): - def write(self, s): - pass - - def flush(self): - pass - - -class StdoutMock(object): - def readline(self): - pass - - def read(self): - pass +import pytest JSON_RPC_RESULT_LIST = [ 'Content-Length: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode("utf-8"), 'Content-Length: 40\r\n\r\n{"key_num": 1, "key_str": "some_string"}'.encode("utf-8") ] + def test_send_sanity(): pipein, pipeout = os.pipe() pipein = os.fdopen(pipein, "rb") @@ -67,8 +51,21 @@ def test_recv_wrong_header(): json_rpc_endpoint = pylspclient.JsonRpcEndpoint(None, pipein) pipeout.write('Contentength: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode("utf-8")) pipeout.flush() - with pytest.raises(RuntimeError): + with pytest.raises(pylspclient.lsp_structs.ResponseError): result = json_rpc_endpoint.recv_response() + print("should never get here", result) + + +def test_recv_missing_size(): + pipein, pipeout = os.pipe() + pipein = os.fdopen(pipein, "rb") + pipeout = os.fdopen(pipeout, "wb") + json_rpc_endpoint = pylspclient.JsonRpcEndpoint(None, pipein) + pipeout.write('Content-Type: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode("utf-8")) + pipeout.flush() + with pytest.raises(pylspclient.lsp_structs.ResponseError): + result = json_rpc_endpoint.recv_response() + print("should never get here", result) def test_recv_close_pipe():