diff --git a/examples/clangd.py b/examples/clangd.py new file mode 100644 index 0000000..cc7898a --- /dev/null +++ b/examples/clangd.py @@ -0,0 +1,145 @@ +import pylspclient +import subprocess + + +if __name__ == "__main__": + # clangd_path = "/usr/bin/clangd-6.0" + clangd_path = "/home/osboxes/projects/build/bin/clangd" + p = subprocess.Popen(clangd_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + json_rpc_endpoint = pylspclient.JsonRpcEndpoint(p.stdin, p.stdout) + # Working with socket: + # sock_fd = sock.makefile() + # json_rpc_endpoint = JsonRpcEndpoint(sock_fd, stext_document_res = lpc_client.send_notification(text_document_message)ock_fd) + lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint) + + lsp_client = pylspclient.LspClient(lsp_endpoint) + capabilities = {'textDocument': {'codeAction': {'dynamicRegistration': True}, + 'codeLens': {'dynamicRegistration': True}, + 'colorProvider': {'dynamicRegistration': True}, + 'completion': {'completionItem': {'commitCharactersSupport': True, + 'documentationFormat': ['markdown', 'plaintext'], + 'snippetSupport': True}, + 'completionItemKind': {'valueSet': [1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25]}, + 'contextSupport': True, + 'dynamicRegistration': True}, + 'definition': {'dynamicRegistration': True}, + 'documentHighlight': {'dynamicRegistration': True}, + 'documentLink': {'dynamicRegistration': True}, + 'documentSymbol': {'dynamicRegistration': True, + 'symbolKind': {'valueSet': [1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26]}}, + 'formatting': {'dynamicRegistration': True}, + 'hover': {'contentFormat': ['markdown', 'plaintext'], + 'dynamicRegistration': True}, + 'implementation': {'dynamicRegistration': True}, + 'onTypeFormatting': {'dynamicRegistration': True}, + 'publishDiagnostics': {'relatedInformation': True}, + 'rangeFormatting': {'dynamicRegistration': True}, + 'references': {'dynamicRegistration': True}, + 'rename': {'dynamicRegistration': True}, + 'signatureHelp': {'dynamicRegistration': True, + 'signatureInformation': {'documentationFormat': ['markdown', 'plaintext']}}, + 'synchronization': {'didSave': True, + 'dynamicRegistration': True, + 'willSave': True, + 'willSaveWaitUntil': True}, + 'typeDefinition': {'dynamicRegistration': True}}, + 'workspace': {'applyEdit': True, + 'configuration': True, + 'didChangeConfiguration': {'dynamicRegistration': True}, + 'didChangeWatchedFiles': {'dynamicRegistration': True}, + 'executeCommand': {'dynamicRegistration': True}, + 'symbol': {'dynamicRegistration': True, + 'symbolKind': {'valueSet': [1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26]}},'workspaceEdit': {'documentChanges': True}, + 'workspaceFolders': True}} + workspace_folders = [{'name': 'python-lsp', 'uri': 'file:///home/osboxes/projects/ctest'}] + root_uri = 'file:///home/osboxes/projects/ctest' + print(lsp_client.initialize(p.pid, None, root_uri, None, capabilities, "off", workspace_folders)) + print(lsp_client.initialized()) + + file_path = "/home/osboxes/projects/ctest/test.c" + uri = "file://" + file_path + text = open(file_path, "r").read() + languageId = pylspclient.lsp_structs.LANGUAGE_IDENTIFIER.C + version = 1 + lsp_client.didOpen(pylspclient.lsp_structs.TextDocumentItem(uri, languageId, version, text)) + lsp_client.documentSymbol(pylspclient.lsp_structs.TextDocumentIdentifier(uri)) + + 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.shutdown() + lsp_client.exit() diff --git a/lsp/__init__.py b/lsp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lsp/json_rpc_client.py b/lsp/json_rpc_client.py deleted file mode 100644 index 8e103a8..0000000 --- a/lsp/json_rpc_client.py +++ /dev/null @@ -1,412 +0,0 @@ -from __future__ import print_function -import subprocess -import json -import re -import threading -import lsp_structs - -JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}\r\n\r\n" -JSON_RPC_RES_REGEX = "Content-Length: ([0-9]*)\r\n" -# TODO: add content-type - - -class MyEncoder(json.JSONEncoder): - """ - Encodes an object in JSON - """ - def default(self, o): - return o.__dict__ - - -class JsonRpcEndpoint(object): - def __init__(self, stdin, stdout): - self.stdin = stdin - self.stdout = stdout - self.read_lock = threading.Lock() - self.write_lock = threading.Lock() - - @staticmethod - def __add_header(json_string): - return JSON_RPC_REQ_FORMAT.format(json_string_len=len(json_string), json_string=json_string) - - - def send_request(self, message): - ''' - :param dict message: The message to send. - ''' - json_string = json.dumps(message, cls=MyEncoder) - print("sending:", json_string) - jsonrpc_req = self.__add_header(json_string) - with self.write_lock: - self.stdin.write(jsonrpc_req.encode()) - self.stdin.flush() - - - def recv_response(self): - ''' - ''' - with self.read_lock: - line = self.stdout.readline() - if line is None: - return None - line = line.decode() - # TODO: handle content type as well. - match = re.match(JSON_RPC_RES_REGEX, line) - if match is None or not match.groups(): - # TODO: handle - print("error1: ", line) - return None - size = int(match.groups()[0]) - line = self.stdout.readline() - if line is None: - return None - line = line.decode() - if line != "\r\n": - # TODO: handle - print("error2") - return None - jsonrpc_res = self.stdout.read(size) - return json.loads(jsonrpc_res) - - -class LspEndpoint(threading.Thread): - def __init__(self, json_rpc_endpoint, default_callback=print, callbacks={}): - threading.Thread.__init__(self) - self.json_rpc_endpoint = json_rpc_endpoint - self.callbacks = callbacks - self.default_callback = default_callback - self.event_dict = {} - self.response_dict = {} - self.next_id = 0 - # self.daemon = True - self.shutdown_flag = False - - - def handle_result(self, jsonrpc_res): - self.response_dict[jsonrpc_res["id"]] = jsonrpc_res - cond = self.event_dict[jsonrpc_res["id"]] - cond.acquire() - cond.notify() - cond.release() - - - def stop(self): - self.shutdown_flag = True - - - def run(self): - while not self.shutdown_flag: - jsonrpc_message = self.json_rpc_endpoint.recv_response() - - if jsonrpc_message is None: - print("server quit") - break - - print("recieved message:", jsonrpc_message) - if "result" in jsonrpc_message or "error" in jsonrpc_message: - self.handle_result(jsonrpc_message) - elif "method" in jsonrpc_message: - if jsonrpc_message["method"] in self.callbacks: - self.callbacks[jsonrpc_message["method"]](jsonrpc_message) - else: - self.default_callback(jsonrpc_message) - else: - print("unknown jsonrpc message") - print(jsonrpc_message) - - - def send_message(self, method_name, params, id = None): - message_dict = {} - message_dict["jsonrpc"] = "2.0" - if id is not None: - message_dict["id"] = id - message_dict["method"] = method_name - message_dict["params"] = params - self.json_rpc_endpoint.send_request(message_dict) - - - def call_method(self, method_name, **kwargs): - current_id = self.next_id - self.next_id += 1 - cond = threading.Condition() - self.event_dict[current_id] = cond - cond.acquire() - self.send_message(method_name, kwargs, current_id) - cond.wait() - cond.release() - # TODO: check if error, and throw an exception - response = self.response_dict[current_id] - return response["result"] - - - def send_notification(self, method_name, **kwargs): - self.send_message(method_name, kwargs) - - -class LspClient(object): - def __init__(self, lpc_endpoint): - self.lpc_endpoint = lpc_endpoint - - - def initialize(self, processId, rootPath, rootUri, initializationOptions, capabilities, trace, workspaceFolders): - """ - The initialize request is sent as the first request from the client to the server. If the server receives a request or notification - before the initialize request it should act as follows: - - 1. For a request the response should be an error with code: -32002. The message can be picked by the server. - 2. Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request. - - Until the server has responded to the initialize request with an InitializeResult, the client must not send any additional requests or - notifications to the server. In addition the server is not allowed to send any requests or notifications to the client until it has responded - with an InitializeResult, with the exception that during the initialize request the server is allowed to send the notifications window/showMessage, - window/logMessage and telemetry/event as well as the window/showMessageRequest request to the client. - - The initialize request may only be sent once. - - :param int processId: The process Id of the parent process that started the server. Is null if the process has not been started by another process. - If the parent process is not alive then the server should exit (see exit notification) its process. - :param str rootPath: The rootPath of the workspace. Is null if no folder is open. Deprecated in favour of rootUri. - :param DocumentUri rootUri: The rootUri of the workspace. Is null if no folder is open. If both `rootPath` and `rootUri` are set - `rootUri` wins. - :param any initializationOptions: User provided initialization options. - :param ClientCapabilities capabilities: The capabilities provided by the client (editor or tool). - :param Trace trace: The initial trace setting. If omitted trace is disabled ('off'). - :param list workspaceFolders: The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. - It can be `null` if the client supports workspace folders but none are configured. - """ - lsp_endpoint.start() - return self.lpc_endpoint.call_method("initialize", processId=processId, rootPath=rootPath, rootUri=rootUri, initializationOptions=initializationOptions, capabilities=capabilities, trace=trace, workspaceFolders=workspaceFolders) - - - def initialized(self): - """ - The initialized notification is sent from the client to the server after the client received the result of the initialize request - but before the client is sending any other request or notification to the server. The server can use the initialized notification - for example to dynamically register capabilities. The initialized notification may only be sent once. - """ - self.lpc_endpoint.send_notification("initialized") - - - def shutdown(self): - """ - The initialized notification is sent from the client to the server after the client received the result of the initialize request - but before the client is sending any other request or notification to the server. The server can use the initialized notification - for example to dynamically register capabilities. The initialized notification may only be sent once. - """ - lsp_endpoint.stop() - return self.lpc_endpoint.call_method("shutdown") - - - def exit(self): - """ - The initialized notification is sent from the client to the server after the client received the result of the initialize request - but before the client is sending any other request or notification to the server. The server can use the initialized notification - for example to dynamically register capabilities. The initialized notification may only be sent once. - """ - self.lpc_endpoint.send_notification("exit") - - - def didOpen(self, textDocument): - """ - The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is - now managed by the client and the server must not try to read the document's truth using the document's uri. Open in this sense - means it is managed by the client. It doesn't necessarily mean that its content is presented in an editor. An open notification must - not be sent more than once without a corresponding close notification send before. This means open and close notification must be - balanced and the max open count for a particular textDocument is one. Note that a server's ability to fulfill requests is independent - of whether a text document is open or closed. - - The DidOpenTextDocumentParams contain the language id the document is associated with. If the language Id of a document changes, the - client needs to send a textDocument/didClose to the server followed by a textDocument/didOpen with the new language id if the server - handles the new language id as well. - - :param TextDocumentItem textDocument: The initial trace setting. If omitted trace is disabled ('off'). - """ - return self.lpc_endpoint.send_notification("textDocument/didOpen", textDocument=textDocument) - - - def documentSymbol(self, textDocument): - """ - The document symbol request is sent from the client to the server to return a flat list of all symbols found in a given text document. - Neither the symbol's location range nor the symbol's container name should be used to infer a hierarchy. - - :param TextDocumentItem textDocument: The text document. - """ - result_dict = self.lpc_endpoint.call_method("textDocument/documentSymbol", textDocument=textDocument) - return [lsp_structs.SymbolInformation(**sym) for sym in result_dict] - - - def definition(self, textDocument, position): - """ - The goto definition request is sent from the client to the server to resolve the definition location of a symbol at a given text document position. - - :param TextDocumentItem textDocument: The text document. - :param Position position: The position inside the text document.. - """ - result_dict = self.lpc_endpoint.call_method("textDocument/definition", textDocument=textDocument, position=position) - return [lsp_structs.Location(**l) for l in result_dict] - - - def typeDefinition(self, textDocument, position): - """ - The goto type definition request is sent from the client to the server to resolve the type definition location of a symbol at a given text document position. - - :param TextDocumentItem textDocument: The text document. - :param Position position: The position inside the text document.. - """ - result_dict = self.lpc_endpoint.call_method("textDocument/definition", textDocument=textDocument, position=position) - return [lsp_structs.Location(**l) for l in result_dict] - - - def signatureHelp(self, textDocument, position): - """ - The signature help request is sent from the client to the server to request signature information at a given cursor position. - - :param TextDocumentItem textDocument: The text document. - :param Position position: The position inside the text document.. - """ - result_dict = self.lpc_endpoint.call_method("textDocument/signatureHelp", textDocument=textDocument, position=position) - return lsp_structs.SignatureHelp(**result_dict) - - -########################################### Example Start - -# clangd_path = "/usr/bin/clangd-6.0" -clangd_path = "/home/osboxes/projects/build/bin/clangd" -p = subprocess.Popen(clangd_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -json_rpc_endpoint = JsonRpcEndpoint(p.stdin, p.stdout) -# Working with socket: -# sock_fd = sock.makefile() -# json_rpc_endpoint = JsonRpcEndpoint(sock_fd, stext_document_res = lpc_client.send_notification(text_document_message)ock_fd) -lsp_endpoint = LspEndpoint(json_rpc_endpoint) - -lsp_client = LspClient(lsp_endpoint) -capabilities = {'textDocument': {'codeAction': {'dynamicRegistration': True}, - 'codeLens': {'dynamicRegistration': True}, - 'colorProvider': {'dynamicRegistration': True}, - 'completion': {'completionItem': {'commitCharactersSupport': True, - 'documentationFormat': ['markdown', 'plaintext'], - 'snippetSupport': True}, - 'completionItemKind': {'valueSet': [1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25]}, - 'contextSupport': True, - 'dynamicRegistration': True}, - 'definition': {'dynamicRegistration': True}, - 'documentHighlight': {'dynamicRegistration': True}, - 'documentLink': {'dynamicRegistration': True}, - 'documentSymbol': {'dynamicRegistration': True, - 'symbolKind': {'valueSet': [1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26]}}, - 'formatting': {'dynamicRegistration': True}, - 'hover': {'contentFormat': ['markdown', 'plaintext'], - 'dynamicRegistration': True}, - 'implementation': {'dynamicRegistration': True}, - 'onTypeFormatting': {'dynamicRegistration': True}, - 'publishDiagnostics': {'relatedInformation': True}, - 'rangeFormatting': {'dynamicRegistration': True}, - 'references': {'dynamicRegistration': True}, - 'rename': {'dynamicRegistration': True}, - 'signatureHelp': {'dynamicRegistration': True, - 'signatureInformation': {'documentationFormat': ['markdown', 'plaintext']}}, - 'synchronization': {'didSave': True, - 'dynamicRegistration': True, - 'willSave': True, - 'willSaveWaitUntil': True}, - 'typeDefinition': {'dynamicRegistration': True}}, - 'workspace': {'applyEdit': True, - 'configuration': True, - 'didChangeConfiguration': {'dynamicRegistration': True}, - 'didChangeWatchedFiles': {'dynamicRegistration': True}, - 'executeCommand': {'dynamicRegistration': True}, - 'symbol': {'dynamicRegistration': True, - 'symbolKind': {'valueSet': [1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26]}},'workspaceEdit': {'documentChanges': True}, - 'workspaceFolders': True}} -workspace_folders = [{'name': 'python-lsp', 'uri': 'file:///home/osboxes/projects/ctest'}] -root_uri = 'file:///home/osboxes/projects/ctest' -print(lsp_client.initialize(p.pid, None, root_uri, None, capabilities, "off", workspace_folders)) -print(lsp_client.initialized()) - -file_path = "/home/osboxes/projects/ctest/test.c" -uri = "file://" + file_path -text = open(file_path, "r").read() -languageId = lsp_structs.LANGUAGE_IDENTIFIER.C -version = 1 -lsp_client.didOpen(lsp_structs.TextDocumentItem(uri, languageId, version, text)) -lsp_client.documentSymbol(lsp_structs.TextDocumentIdentifier(uri)) - -lsp_client.definition(lsp_structs.TextDocumentIdentifier(uri), lsp_structs.Position(15, 4)) -lsp_client.signatureHelp(lsp_structs.TextDocumentIdentifier(uri), lsp_structs.Position(15, 4)) - -lsp_client.shutdown() -lsp_client.exit() \ No newline at end of file diff --git a/pylspclient/__init__.py b/pylspclient/__init__.py new file mode 100644 index 0000000..1cb55c7 --- /dev/null +++ b/pylspclient/__init__.py @@ -0,0 +1,8 @@ +#from __future__ import absolute_import + +__all__ = [] + +from pylspclient.json_rpc_endpoint import JsonRpcEndpoint +from pylspclient.lsp_client import LspClient +from pylspclient.lsp_endpoint import LspEndpoint +from pylspclient import lsp_structs diff --git a/pylspclient/json_rpc_endpoint.py b/pylspclient/json_rpc_endpoint.py new file mode 100644 index 0000000..a117360 --- /dev/null +++ b/pylspclient/json_rpc_endpoint.py @@ -0,0 +1,72 @@ +from __future__ import print_function +import json +import re +from pylspclient import lsp_structs +import threading + +JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}\r\n\r\n" +JSON_RPC_RES_REGEX = "Content-Length: ([0-9]*)\r\n" +# TODO: add content-type + + +class MyEncoder(json.JSONEncoder): + """ + Encodes an object in JSON + """ + def default(self, o): + return o.__dict__ + + +class JsonRpcEndpoint(object): + ''' + Thread safe JSON RPC endpoint implementation. Responsible to recieve and send JSON RPC messages, as described in the + protocol. More information can be found: https://www.jsonrpc.org/ + ''' + def __init__(self, stdin, stdout): + self.stdin = stdin + self.stdout = stdout + self.read_lock = threading.Lock() + self.write_lock = threading.Lock() + + @staticmethod + def __add_header(json_string): + return JSON_RPC_REQ_FORMAT.format(json_string_len=len(json_string), json_string=json_string) + + + def send_request(self, message): + ''' + :param dict message: The message to send. + ''' + json_string = json.dumps(message, cls=MyEncoder) + print("sending:", json_string) + jsonrpc_req = self.__add_header(json_string) + with self.write_lock: + self.stdin.write(jsonrpc_req.encode()) + self.stdin.flush() + + + def recv_response(self): + ''' + ''' + with self.read_lock: + line = self.stdout.readline() + if line is None: + return None + line = line.decode() + # TODO: handle content type as well. + match = re.match(JSON_RPC_RES_REGEX, line) + if match is None or not match.groups(): + # TODO: handle + print("error1: ", line) + return None + size = int(match.groups()[0]) + line = self.stdout.readline() + if line is None: + return None + line = line.decode() + if line != "\r\n": + # TODO: handle + print("error2") + return None + jsonrpc_res = self.stdout.read(size) + return json.loads(jsonrpc_res) diff --git a/pylspclient/lsp_client.py b/pylspclient/lsp_client.py new file mode 100644 index 0000000..d6912b6 --- /dev/null +++ b/pylspclient/lsp_client.py @@ -0,0 +1,125 @@ +from pylspclient import lsp_structs + +class LspClient(object): + def __init__(self, lsp_endpoint): + self.lsp_endpoint = lsp_endpoint + + + def initialize(self, processId, rootPath, rootUri, initializationOptions, capabilities, trace, workspaceFolders): + """ + The initialize request is sent as the first request from the client to the server. If the server receives a request or notification + before the initialize request it should act as follows: + + 1. For a request the response should be an error with code: -32002. The message can be picked by the server. + 2. Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request. + + Until the server has responded to the initialize request with an InitializeResult, the client must not send any additional requests or + notifications to the server. In addition the server is not allowed to send any requests or notifications to the client until it has responded + with an InitializeResult, with the exception that during the initialize request the server is allowed to send the notifications window/showMessage, + window/logMessage and telemetry/event as well as the window/showMessageRequest request to the client. + + The initialize request may only be sent once. + + :param int processId: The process Id of the parent process that started the server. Is null if the process has not been started by another process. + If the parent process is not alive then the server should exit (see exit notification) its process. + :param str rootPath: The rootPath of the workspace. Is null if no folder is open. Deprecated in favour of rootUri. + :param DocumentUri rootUri: The rootUri of the workspace. Is null if no folder is open. If both `rootPath` and `rootUri` are set + `rootUri` wins. + :param any initializationOptions: User provided initialization options. + :param ClientCapabilities capabilities: The capabilities provided by the client (editor or tool). + :param Trace trace: The initial trace setting. If omitted trace is disabled ('off'). + :param list workspaceFolders: The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. + It can be `null` if the client supports workspace folders but none are configured. + """ + self.lsp_endpoint.start() + return self.lsp_endpoint.call_method("initialize", processId=processId, rootPath=rootPath, rootUri=rootUri, initializationOptions=initializationOptions, capabilities=capabilities, trace=trace, workspaceFolders=workspaceFolders) + + + def initialized(self): + """ + The initialized notification is sent from the client to the server after the client received the result of the initialize request + but before the client is sending any other request or notification to the server. The server can use the initialized notification + for example to dynamically register capabilities. The initialized notification may only be sent once. + """ + self.lsp_endpoint.send_notification("initialized") + + + def shutdown(self): + """ + The initialized notification is sent from the client to the server after the client received the result of the initialize request + but before the client is sending any other request or notification to the server. The server can use the initialized notification + for example to dynamically register capabilities. The initialized notification may only be sent once. + """ + self.lsp_endpoint.stop() + return self.lsp_endpoint.call_method("shutdown") + + + def exit(self): + """ + The initialized notification is sent from the client to the server after the client received the result of the initialize request + but before the client is sending any other request or notification to the server. The server can use the initialized notification + for example to dynamically register capabilities. The initialized notification may only be sent once. + """ + self.lsp_endpoint.send_notification("exit") + + + def didOpen(self, textDocument): + """ + The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is + now managed by the client and the server must not try to read the document's truth using the document's uri. Open in this sense + means it is managed by the client. It doesn't necessarily mean that its content is presented in an editor. An open notification must + not be sent more than once without a corresponding close notification send before. This means open and close notification must be + balanced and the max open count for a particular textDocument is one. Note that a server's ability to fulfill requests is independent + of whether a text document is open or closed. + + The DidOpenTextDocumentParams contain the language id the document is associated with. If the language Id of a document changes, the + client needs to send a textDocument/didClose to the server followed by a textDocument/didOpen with the new language id if the server + handles the new language id as well. + + :param TextDocumentItem textDocument: The initial trace setting. If omitted trace is disabled ('off'). + """ + return self.lsp_endpoint.send_notification("textDocument/didOpen", textDocument=textDocument) + + + def documentSymbol(self, textDocument): + """ + The document symbol request is sent from the client to the server to return a flat list of all symbols found in a given text document. + Neither the symbol's location range nor the symbol's container name should be used to infer a hierarchy. + + :param TextDocumentItem textDocument: The text document. + """ + result_dict = self.lsp_endpoint.call_method("textDocument/documentSymbol", textDocument=textDocument) + return [lsp_structs.SymbolInformation(**sym) for sym in result_dict] + + + def definition(self, textDocument, position): + """ + The goto definition request is sent from the client to the server to resolve the definition location of a symbol at a given text document position. + + :param TextDocumentItem textDocument: The text document. + :param Position position: The position inside the text document.. + """ + result_dict = self.lsp_endpoint.call_method("textDocument/definition", textDocument=textDocument, position=position) + return [lsp_structs.Location(**l) for l in result_dict] + + + def typeDefinition(self, textDocument, position): + """ + The goto type definition request is sent from the client to the server to resolve the type definition location of a symbol at a given text document position. + + :param TextDocumentItem textDocument: The text document. + :param Position position: The position inside the text document.. + """ + result_dict = self.lsp_endpoint.call_method("textDocument/definition", textDocument=textDocument, position=position) + return [lsp_structs.Location(**l) for l in result_dict] + + + def signatureHelp(self, textDocument, position): + """ + The signature help request is sent from the client to the server to request signature information at a given cursor position. + + :param TextDocumentItem textDocument: The text document. + :param Position position: The position inside the text document.. + """ + result_dict = self.lsp_endpoint.call_method("textDocument/signatureHelp", textDocument=textDocument, position=position) + return lsp_structs.SignatureHelp(**result_dict) diff --git a/pylspclient/lsp_endpoint.py b/pylspclient/lsp_endpoint.py new file mode 100644 index 0000000..a292327 --- /dev/null +++ b/pylspclient/lsp_endpoint.py @@ -0,0 +1,75 @@ +from __future__ import print_function +import threading + +class LspEndpoint(threading.Thread): + def __init__(self, json_rpc_endpoint, default_callback=print, callbacks={}): + threading.Thread.__init__(self) + self.json_rpc_endpoint = json_rpc_endpoint + self.callbacks = callbacks + self.default_callback = default_callback + self.event_dict = {} + self.response_dict = {} + self.next_id = 0 + # self.daemon = True + self.shutdown_flag = False + + + def handle_result(self, jsonrpc_res): + self.response_dict[jsonrpc_res["id"]] = jsonrpc_res + cond = self.event_dict[jsonrpc_res["id"]] + cond.acquire() + cond.notify() + cond.release() + + + def stop(self): + self.shutdown_flag = True + + + def run(self): + while not self.shutdown_flag: + jsonrpc_message = self.json_rpc_endpoint.recv_response() + + if jsonrpc_message is None: + print("server quit") + break + + print("recieved message:", jsonrpc_message) + if "result" in jsonrpc_message or "error" in jsonrpc_message: + self.handle_result(jsonrpc_message) + elif "method" in jsonrpc_message: + if jsonrpc_message["method"] in self.callbacks: + self.callbacks[jsonrpc_message["method"]](jsonrpc_message) + else: + self.default_callback(jsonrpc_message) + else: + print("unknown jsonrpc message") + print(jsonrpc_message) + + + def send_message(self, method_name, params, id = None): + message_dict = {} + message_dict["jsonrpc"] = "2.0" + if id is not None: + message_dict["id"] = id + message_dict["method"] = method_name + message_dict["params"] = params + self.json_rpc_endpoint.send_request(message_dict) + + + def call_method(self, method_name, **kwargs): + current_id = self.next_id + self.next_id += 1 + cond = threading.Condition() + self.event_dict[current_id] = cond + cond.acquire() + self.send_message(method_name, kwargs, current_id) + cond.wait() + cond.release() + # TODO: check if error, and throw an exception + response = self.response_dict[current_id] + return response["result"] + + + def send_notification(self, method_name, **kwargs): + self.send_message(method_name, kwargs) diff --git a/lsp/lsp_structs.py b/pylspclient/lsp_structs.py similarity index 100% rename from lsp/lsp_structs.py rename to pylspclient/lsp_structs.py diff --git a/setup.py b/setup.py index 6e4772d..33bb6b7 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,6 @@ class PyTest(TestCommand): setup( name="pylspclient", packages=find_packages(), - tests_require=["pytest"], + tests_require=["pytest", "pytest_mock"], cmdclass={"test": PyTest}, ) diff --git a/tests/test_a.py b/tests/test_a.py index a9fd22b..d929562 100644 --- a/tests/test_a.py +++ b/tests/test_a.py @@ -1,4 +1,6 @@ -from lsp import json_rpc_client +import pylspclient +from pytest_mock import mocker + # content of test_sample.py def func(x): return x + 1 diff --git a/tox.ini b/tox.ini index 681a04a..d91cfea 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,8 @@ envlist = py27,py36 [testenv] -deps = pytest +deps = + pytest + pytest_mock commands = pytest