From 29b8935ebbb5f54e87a0ff29abc97506fc1c97aa Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 4 Nov 2023 14:38:20 -0500 Subject: [PATCH] Inferastructure work on LSP plugin --- plugins/lsp_client/__init__.py | 3 + plugins/lsp_client/__main__.py | 3 + plugins/lsp_client/capabilities.py | 201 +++++++ plugins/lsp_client/lsp_controller.py | 77 +++ plugins/lsp_client/manifest.json | 16 + plugins/lsp_client/plugin.py | 80 +++ plugins/lsp_client/pylspclient/LICENSE | 21 + plugins/lsp_client/pylspclient/__init__.py | 6 + .../pylspclient/json_rpc_endpoint.py | 105 ++++ plugins/lsp_client/pylspclient/lsp_client.py | 260 ++++++++ .../lsp_client/pylspclient/lsp_endpoint.py | 113 ++++ plugins/lsp_client/pylspclient/lsp_structs.py | 566 ++++++++++++++++++ 12 files changed, 1451 insertions(+) create mode 100644 plugins/lsp_client/__init__.py create mode 100644 plugins/lsp_client/__main__.py create mode 100644 plugins/lsp_client/capabilities.py create mode 100644 plugins/lsp_client/lsp_controller.py create mode 100644 plugins/lsp_client/manifest.json create mode 100644 plugins/lsp_client/plugin.py create mode 100644 plugins/lsp_client/pylspclient/LICENSE create mode 100644 plugins/lsp_client/pylspclient/__init__.py create mode 100644 plugins/lsp_client/pylspclient/json_rpc_endpoint.py create mode 100644 plugins/lsp_client/pylspclient/lsp_client.py create mode 100644 plugins/lsp_client/pylspclient/lsp_endpoint.py create mode 100644 plugins/lsp_client/pylspclient/lsp_structs.py diff --git a/plugins/lsp_client/__init__.py b/plugins/lsp_client/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/lsp_client/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/lsp_client/__main__.py b/plugins/lsp_client/__main__.py new file mode 100644 index 0000000..a576329 --- /dev/null +++ b/plugins/lsp_client/__main__.py @@ -0,0 +1,3 @@ +""" + Pligin Package +""" diff --git a/plugins/lsp_client/capabilities.py b/plugins/lsp_client/capabilities.py new file mode 100644 index 0000000..8cd31d2 --- /dev/null +++ b/plugins/lsp_client/capabilities.py @@ -0,0 +1,201 @@ +# Python imports + +# Lib imports + +# Application imports + + + +class Capabilities: + data = { + "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 + } +} \ No newline at end of file diff --git a/plugins/lsp_client/lsp_controller.py b/plugins/lsp_client/lsp_controller.py new file mode 100644 index 0000000..2a54af2 --- /dev/null +++ b/plugins/lsp_client/lsp_controller.py @@ -0,0 +1,77 @@ +# Python imports +import subprocess +import threading + +# Lib imports +from . import pylspclient + +# Application imports +from .capabilities import Capabilities + + + +class ReadPipe(threading.Thread): + def __init__(self, pipe): + threading.Thread.__init__(self) + self.pipe = pipe + + def run(self): + line = self.pipe.readline().decode('utf-8') + while line: + line = self.pipe.readline().decode('utf-8') + + + +class LSPController: + def __init__(self): + super().__init__() + + self.lsp_clients = [] + + + def create_client(self, language = "", server_proc = None): + if not language or not server_proc: return False + + json_rpc_endpoint = pylspclient.JsonRpcEndpoint(server_proc.stdin, server_proc.stdout) + lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint) + lsp_client = pylspclient.LspClient(lsp_endpoint) + + self.lsp_clients.append(lsp_client) + + root_path = None + root_uri = 'file:///home/abaddon/Coding/Projects/Active/C_n_CPP_Projects/gtk/Newton/src/' + workspace_folders = [{'name': 'python-lsp', 'uri': root_uri}] + + lsp_client.initialize( + processId = server_proc.pid, \ + rootPath = root_path, \ + rootUri = root_uri, \ + initializationOptions = None, \ + capabilities = Capabilities.data, \ + trace = "off", \ + workspaceFolders = workspace_folders + ) + + lsp_client.initialized() + + return True + + def create_lsp_server(self, server_command: [] = []): + if not server_command: return None + + server_proc = subprocess.Popen(server_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + read_pipe = ReadPipe(server_proc.stderr) + read_pipe.start() + + + return server_proc + + def _shutting_down(self): + for lsp_client in self.lsp_clients: + lsp_client.shutdown() + lsp_client.exit() + + + + + diff --git a/plugins/lsp_client/manifest.json b/plugins/lsp_client/manifest.json new file mode 100644 index 0000000..9a5072f --- /dev/null +++ b/plugins/lsp_client/manifest.json @@ -0,0 +1,16 @@ +{ + "manifest": { + "name": "LSP Client", + "author": "ITDominator", + "version": "0.0.1", + "credit": "Avi Yeger for the pylspclient used by this plugin. Link: https://github.com/yeger00/pylspclient", + "support": "", + "requests": { + "pass_events": "true", + "bind_keys": [ + "LSP Client||do_goto:g", + "LSP Client||do_get_implementation:n" + ] + } + } +} \ No newline at end of file diff --git a/plugins/lsp_client/plugin.py b/plugins/lsp_client/plugin.py new file mode 100644 index 0000000..3571cc8 --- /dev/null +++ b/plugins/lsp_client/plugin.py @@ -0,0 +1,80 @@ +# Python imports + +# Lib imports + +# Application imports + + +from plugins.plugin_base import PluginBase +from .lsp_controller import LSPController + + + +class Plugin(PluginBase): + def __init__(self): + super().__init__() + + self.name = "LSP Client" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus + # where self.name should not be needed for message comms + self.lsp_controller = None + + + def generate_reference_ui_element(self): + ... + + def run(self): + self.lsp_controller = LSPController() + server_proc = self.lsp_controller.create_lsp_server(["/usr/bin/clangd"]) + client_created = self.lsp_controller.create_client("c,cpp", server_proc) + if not client_created: + file_type = "dummy" + text = f"LSP could not be created for file type: {file_type} ..." + self._event_system.emit("bubble_message", ("warning", self.name, text,)) + + # language_id = pylspclient.lsp_structs.LANGUAGE_IDENTIFIER.C + # version = 1 + # self.lsp_client.didOpen(pylspclient.lsp_structs.TextDocumentItem(uri, language_id, version, text)) + # try: + # symbols = self.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") + # ... + + # self.lsp_client.definition(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4)) + # self.lsp_client.signatureHelp(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4)) + # self.lsp_client.definition(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4)) + # self.lsp_client.completion(pylspclient.lsp_structs.TextDocumentIdentifier(uri), pylspclient.lsp_structs.Position(14, 4), pylspclient.lsp_structs.CompletionContext(pylspclient.lsp_structs.CompletionTriggerKind.Invoked)) + + + + def subscribe_to_events(self): + self._event_system.subscribe("shutting_down", self._shutting_down) + self._event_system.subscribe("set_active_src_view", self._set_active_src_view) + self._event_system.subscribe("buffer_changed_first_load", self._buffer_changed_first_load) + self._event_system.subscribe("buffer_changed", self._buffer_changed) + + self._event_system.subscribe("do_goto", self._do_goto) + self._event_system.subscribe("do_get_implementation", self._do_get_implementation) + + def _shutting_down(self): + self.lsp_controller._shutting_down() + + def _set_active_src_view(self, source_view): + self._active_src_view = source_view + self._buffer = source_view.get_buffer() + self._file_type = source_view.get_filetype() + + def _buffer_changed_first_load(self, buffer): + self._buffer = buffer + + def _buffer_changed(self, buffer): + ... + + def _do_goto(self): + ... + + def _do_get_implementation(self): + ... \ No newline at end of file diff --git a/plugins/lsp_client/pylspclient/LICENSE b/plugins/lsp_client/pylspclient/LICENSE new file mode 100644 index 0000000..0728ddb --- /dev/null +++ b/plugins/lsp_client/pylspclient/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Avi Yeger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/lsp_client/pylspclient/__init__.py b/plugins/lsp_client/pylspclient/__init__.py new file mode 100644 index 0000000..58572a1 --- /dev/null +++ b/plugins/lsp_client/pylspclient/__init__.py @@ -0,0 +1,6 @@ +__all__ = [] + +from .json_rpc_endpoint import JsonRpcEndpoint +from .lsp_client import LspClient +from .lsp_endpoint import LspEndpoint +from . import lsp_structs \ No newline at end of file diff --git a/plugins/lsp_client/pylspclient/json_rpc_endpoint.py b/plugins/lsp_client/pylspclient/json_rpc_endpoint.py new file mode 100644 index 0000000..30561bb --- /dev/null +++ b/plugins/lsp_client/pylspclient/json_rpc_endpoint.py @@ -0,0 +1,105 @@ +from __future__ import print_function + +import threading +import json + +from . import lsp_structs + + + +JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}" +LEN_HEADER = "Content-Length: " +TYPE_HEADER = "Content-Type: " + + +# TODO: add content-type + + +class MyEncoder(json.JSONEncoder): + """ + Encodes an object in JSON + """ + + def default(self, o): # pylint: disable=E0202 + 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): + ''' + Adds a header for the given json string + + :param str json_string: The string + :return: the string with the header + ''' + return JSON_RPC_REQ_FORMAT.format(json_string_len = len(json_string), json_string = json_string) + + def send_request(self, message): + ''' + Sends the given message. + + :param dict message: The message to send. + ''' + json_string = json.dumps(message, cls = MyEncoder) + jsonrpc_req = self.__add_header(json_string) + with self.write_lock: + self.stdin.write(jsonrpc_req.encode()) + self.stdin.flush() + + def recv_response(self): + ''' + Recives a message. + + :return: a message + ''' + with self.read_lock: + 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): + ... + 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) \ No newline at end of file diff --git a/plugins/lsp_client/pylspclient/lsp_client.py b/plugins/lsp_client/pylspclient/lsp_client.py new file mode 100644 index 0000000..1fe9126 --- /dev/null +++ b/plugins/lsp_client/pylspclient/lsp_client.py @@ -0,0 +1,260 @@ +from . import lsp_structs + + + +class LspClient(object): + def __init__(self, lsp_endpoint): + """ + Constructs a new LspClient instance. + + :param lsp_endpoint: TODO + """ + + 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. + """ + + 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") + self.lsp_endpoint.stop() + + + 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 document that was opened. + """ + + return self.lsp_endpoint.send_notification("textDocument/didOpen", textDocument = textDocument) + + def didChange(self, textDocument, contentChanges): + """ + The document change notification is sent from the client to the server to signal changes to a text document. + In 2.0 the shape of the params has changed to include proper version numbers and language ids. + + :param VersionedTextDocumentIdentifier textDocument: The initial trace setting. If omitted trace is disabled ('off'). + :param TextDocumentContentChangeEvent[] contentChanges: The actual content changes. The content changes describe single state changes + to the document. So if there are two content changes c1 and c2 for a document in state S then c1 move the document + to S' and c2 to S''. + """ + + return self.lsp_endpoint.send_notification("textDocument/didChange", textDocument = textDocument, contentChanges = contentChanges) + + 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 declaration(self, textDocument, position): + """ + The go to declaration request is sent from the client to the server to + resolve the declaration location of a symbol at a given text document + position. + + The result type LocationLink[] got introduce with version 3.14.0 and + depends in the corresponding client capability + `clientCapabilities.textDocument.declaration.linkSupport`. + + :param TextDocumentItem textDocument: The text document. + :param Position position: The position inside the text document. + """ + + result_dict = self.lsp_endpoint.call_method("textDocument/declaration", + textDocument = textDocument, + position = position + ) + + if "uri" in result_dict: + return lsp_structs.Location(**result_dict) + + return [lsp_structs.Location(**loc) if "uri" in loc else lsp_structs.LinkLocation(**loc) for loc 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(**loc) for loc 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(**loc) for loc 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) + + def completion(self, textDocument, position, context): + """ + 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. + :param CompletionContext context: The completion context. This is only + available if the client specifies + to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` + """ + + result_dict = self.lsp_endpoint.call_method("textDocument/completion", + textDocument = textDocument, + position = position, + context = context + ) + if "isIncomplete" in result_dict: + return lsp_structs.CompletionList(**result_dict) + + return [lsp_structs.CompletionItem(**loc) for loc in result_dict] + + def definition(self, textDocument, position): + """ + The go to definition request is sent from the client to the server to + resolve the declaration location of a symbol at a given text document + position. + + The result type LocationLink[] got introduce with version 3.14.0 and + depends in the corresponding client capability + `clientCapabilities.textDocument.declaration.linkSupport`. + + :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) + + if "uri" in result_dict: + return lsp_structs.Location(**result_dict) + + return [lsp_structs.Location(**l) + if "uri" in l else lsp_structs.LinkLocation(**l) for l in result_dict] + + def references(self, textDocument, position): + """ + The references request is sent from the client to the server to resolve + project-wide references for the symbol denoted by the 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/references", + textDocument = textDocument, + position = position) + + return [lsp_structs.Location(**loc) for loc in result_dict] \ No newline at end of file diff --git a/plugins/lsp_client/pylspclient/lsp_endpoint.py b/plugins/lsp_client/pylspclient/lsp_endpoint.py new file mode 100644 index 0000000..2ddeab2 --- /dev/null +++ b/plugins/lsp_client/pylspclient/lsp_endpoint.py @@ -0,0 +1,113 @@ +from __future__ import print_function + +import threading + +from . import lsp_structs + + + +class LspEndpoint(threading.Thread): + def __init__(self, json_rpc_endpoint, method_callbacks = {}, notify_callbacks = {}, timeout = 2): + threading.Thread.__init__(self) + self.json_rpc_endpoint = json_rpc_endpoint + self.notify_callbacks = notify_callbacks + self.method_callbacks = method_callbacks + self.event_dict = {} + self.response_dict = {} + self.next_id = 0 + self._timeout = timeout + self.shutdown_flag = False + + def handle_result(self, rpc_id, result, error): + self.response_dict[rpc_id] = (result, error) + cond = self.event_dict[rpc_id] + cond.acquire() + cond.notify() + cond.release() + + def stop(self): + self.shutdown_flag = True + + def run(self): + while not self.shutdown_flag: + try: + jsonrpc_message = self.json_rpc_endpoint.recv_response() + if jsonrpc_message is None: + print("LSP Server Shutting Down...") + break + + method = jsonrpc_message.get("method") + result = jsonrpc_message.get("result") + error = jsonrpc_message.get("error") + rpc_id = jsonrpc_message.get("id") + params = jsonrpc_message.get("params") + + if method: + if rpc_id: + if method not in self.method_callbacks: + 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: + if method not in self.notify_callbacks: + 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: + self.send_response(rpc_id, None, e) + + def send_response(self, id, result, error): + message_dict = {} + message_dict["jsonrpc"] = "2.0" + message_dict["id"] = id + + if result: + message_dict["result"] = result + if error: + message_dict["error"] = error + + self.json_rpc_endpoint.send_request(message_dict) + + 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) + if self.shutdown_flag: + return None + + if not cond.wait(timeout=self._timeout): + raise TimeoutError() + cond.release() + + self.event_dict.pop(current_id) + result, error = self.response_dict.pop(current_id) + if error: + raise lsp_structs.ResponseError(error.get("code"), + error.get("message"), + error.get("data")) + + return result + + def send_notification(self, method_name, **kwargs): + self.send_message(method_name, kwargs) \ No newline at end of file diff --git a/plugins/lsp_client/pylspclient/lsp_structs.py b/plugins/lsp_client/pylspclient/lsp_structs.py new file mode 100644 index 0000000..7a61581 --- /dev/null +++ b/plugins/lsp_client/pylspclient/lsp_structs.py @@ -0,0 +1,566 @@ +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. + + :param object|dict o: The object to convert + :param Type new_type: The type to convert to. + ''' + + return o if new_type == type(o) else new_type(**o) + + +class Position(object): + def __init__(self, line, character): + """ + Constructs a new Position instance. + + :param int line: Line position in a document (zero-based). + :param int character: Character offset on a line in a document + (zero-based). + """ + self.line = line + self.character = character + + +class Range(object): + def __init__(self, start, end): + """ + Constructs a new Range instance. + + :param Position start: The range's start position. + :param Position end: The range's end position. + """ + self.start = to_type(start, Position) + self.end = to_type(end, Position) + + +class Location(object): + """ + Represents a location inside a resource, such as a line inside a text file. + """ + + def __init__(self, uri, range): + """ + Constructs a new Location instance. + + :param str uri: Resource file. + :param Range range: The range inside the file + """ + self.uri = uri + self.range = to_type(range, Range) + + +class LocationLink(object): + """ + Represents a link between a source and a target location. + """ + + def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange): + """ + Constructs a new LocationLink instance. + + :param Range originSelectionRange: Span of the origin of this link. + Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position. + :param str targetUri: The target resource identifier of this link. + :param Range targetRange: The full target range of this link. If the target for example is a symbol then target + range is the range enclosing this symbol not including leading/trailing whitespace but everything else + like comments. This information is typically used to highlight the range in the editor. + :param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed, + e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range` + """ + self.originSelectionRange = to_type(originSelectionRange, Range) + self.targetUri = targetUri + self.targetRange = to_type(targetRange, Range) + self.targetSelectionRange = to_type(targetSelectionRange, Range) + + +class Diagnostic(object): + def __init__(self, range, severity, code, source, message, relatedInformation): + """ + Constructs a new Diagnostic instance. + :param Range range: The range at which the message applies.Resource file. + :param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the + client to interpret diagnostics as error, warning, info or hint. + :param str code: The diagnostic's code, which might appear in the user interface. + :param str source: A human-readable string describing the source of this + diagnostic, e.g. 'typescript' or 'super lint'. + :param str message: The diagnostic's message. + :param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within + a scope collide all definitions can be marked via this property. + """ + self.range = range + self.severity = severity + self.code = code + self.source = source + self.message = message + self.relatedInformation = relatedInformation + + +class DiagnosticSeverity(object): + Error = 1 + Warning = 2 # TODO: warning is known in python + Information = 3 + Hint = 4 + + +class DiagnosticRelatedInformation(object): + def __init__(self, location, message): + """ + Constructs a new Diagnostic instance. + :param Location location: The location of this related diagnostic information. + :param str message: The message of this related diagnostic information. + """ + self.location = location + self.message = message + + +class Command(object): + def __init__(self, title, command, arguments): + """ + Constructs a new Diagnostic instance. + :param str title: Title of the command, like `save`. + :param str command: The identifier of the actual command handler. + :param list argusments: Arguments that the command handler should be invoked with. + """ + self.title = title + self.command = command + self.arguments = arguments + + +class TextDocumentItem(object): + """ + An item to transfer a text document from the client to the server. + """ + def __init__(self, uri, languageId, version, text): + """ + Constructs a new Diagnostic instance. + + :param DocumentUri uri: Title of the command, like `save`. + :param str languageId: The identifier of the actual command handler. + :param int version: Arguments that the command handler should be invoked with. + :param str text: Arguments that the command handler should be invoked with. + """ + self.uri = uri + self.languageId = languageId + self.version = version + self.text = text + + +class TextDocumentIdentifier(object): + """ + Text documents are identified using a URI. On the protocol level, URIs are passed as strings. + """ + def __init__(self, uri): + """ + Constructs a new TextDocumentIdentifier instance. + + :param DocumentUri uri: The text document's URI. + """ + self.uri = uri + + +class VersionedTextDocumentIdentifier(TextDocumentIdentifier): + """ + An identifier to denote a specific version of a text document. + """ + def __init__(self, uri, version): + """ + Constructs a new TextDocumentIdentifier instance. + + :param DocumentUri uri: The text document's URI. + :param int version: The version number of this document. If a versioned + text document identifier is sent from the server to the client and + the file is not open in the editor (the server has not received an + open notification before) the server can send `null` to indicate + that the version is known and the content on disk is the truth (as + speced with document content ownership). + The version number of a document will increase after each change, including + undo/redo. The number doesn't need to be consecutive. + """ + super(VersionedTextDocumentIdentifier, self).__init__(uri) + self.version = version + + +class TextDocumentContentChangeEvent(object): + """ + An event describing a change to a text document. If range and rangeLength are omitted + the new text is considered to be the full content of the document. + """ + def __init__(self, range, rangeLength, text): + """ + Constructs a new TextDocumentContentChangeEvent instance. + + :param Range range: The range of the document that changed. + :param int rangeLength: The length of the range that got replaced. + :param str text: The new text of the range/document. + """ + self.range = range + self.rangeLength = rangeLength + self.text = text + + +class TextDocumentPositionParams(object): + """ + A parameter literal used in requests to pass a text document and a position inside that document. + """ + def __init__(self, textDocument, position): + """ + Constructs a new TextDocumentPositionParams instance. + + :param TextDocumentIdentifier textDocument: The text document. + :param Position position: The position inside the text document. + """ + self.textDocument = textDocument + self.position = position + + +class LANGUAGE_IDENTIFIER(object): + BAT = "bat" + BIBTEX = "bibtex" + CLOJURE = "clojure" + COFFESCRIPT = "coffeescript" + C = "c" + CPP = "cpp" + CSHARP = "csharp" + CSS = "css" + DIFF = "diff" + DOCKERFILE = "dockerfile" + FSHARP = "fsharp" + GIT_COMMIT = "git-commit" + GIT_REBASE = "git-rebase" + GO = "go" + GROOVY = "groovy" + HANDLEBARS = "handlebars" + HTML = "html" + INI = "ini" + JAVA = "java" + JAVASCRIPT = "javascript" + JSON = "json" + LATEX = "latex" + LESS = "less" + LUA = "lua" + MAKEFILE = "makefile" + MARKDOWN = "markdown" + OBJECTIVE_C = "objective-c" + OBJECTIVE_CPP = "objective-cpp" + Perl = "perl" + PHP = "php" + POWERSHELL = "powershell" + PUG = "jade" + PYTHON = "python" + R = "r" + RAZOR = "razor" + RUBY = "ruby" + RUST = "rust" + SASS = "sass" + SCSS = "scss" + ShaderLab = "shaderlab" + SHELL_SCRIPT = "shellscript" + SQL = "sql" + SWIFT = "swift" + TYPE_SCRIPT = "typescript" + TEX = "tex" + VB = "vb" + XML = "xml" + XSL = "xsl" + YAML = "yaml" + + +class SymbolKind(enum.Enum): + File = 1 + Module = 2 + Namespace = 3 + Package = 4 + Class = 5 + Method = 6 + Property = 7 + Field = 8 + Constructor = 9 + Enum = 10 + Interface = 11 + Function = 12 + Variable = 13 + Constant = 14 + String = 15 + Number = 16 + Boolean = 17 + Array = 18 + Object = 19 + Key = 20 + Null = 21 + EnumMember = 22 + Struct = 23 + Event = 24 + Operator = 25 + TypeParameter = 26 + + +class SymbolInformation(object): + """ + Represents information about programming constructs like variables, classes, interfaces etc. + """ + def __init__(self, name, kind, location, containerName = None, deprecated = False): + """ + Constructs a new SymbolInformation instance. + + :param str name: The name of this symbol. + :param int kind: The kind of this symbol. + :param bool Location: The location of this symbol. The location's range is used by a tool + to reveal the location in the editor. If the symbol is selected in the + tool the range's start information is used to position the cursor. So + the range usually spans more then the actual symbol's name and does + normally include things like visibility modifiers. + + The range doesn't have to denote a node range in the sense of a abstract + syntax tree. It can therefore not be used to re-construct a hierarchy of + the symbols. + :param str containerName: The name of the symbol containing this symbol. This information is for + user interface purposes (e.g. to render a qualifier in the user interface + if necessary). It can't be used to re-infer a hierarchy for the document + symbols. + :param bool deprecated: Indicates if this symbol is deprecated. + """ + self.name = name + self.kind = SymbolKind(kind) + self.deprecated = deprecated + self.location = to_type(location, Location) + self.containerName = containerName + + +class ParameterInformation(object): + """ + Represents a parameter of a callable-signature. A parameter can + have a label and a doc-comment. + """ + def __init__(self, label, documentation = ""): + """ + Constructs a new ParameterInformation instance. + + :param str label: The label of this parameter. Will be shown in the UI. + :param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted. + """ + self.label = label + self.documentation = documentation + + +class SignatureInformation(object): + """ + Represents the signature of something callable. A signature + can have a label, like a function-name, a doc-comment, and + a set of parameters. + """ + def __init__(self, label, documentation = "", parameters = []): + """ + Constructs a new SignatureInformation instance. + + :param str label: The label of this signature. Will be shown in the UI. + :param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted. + :param ParameterInformation[] parameters: The parameters of this signature. + """ + self.label = label + self.documentation = documentation + self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters] + + +class SignatureHelp(object): + """ + Signature help represents the signature of something + callable. There can be multiple signature but only one + active and only one active parameter. + """ + def __init__(self, signatures, activeSignature = 0, activeParameter = 0): + """ + Constructs a new SignatureHelp instance. + + :param SignatureInformation[] signatures: One or more signatures. + :param int activeSignature: + :param int activeParameter: + """ + self.signatures = [to_type(signature, SignatureInformation) for signature in signatures] + self.activeSignature = activeSignature + self.activeParameter = activeParameter + + +class CompletionTriggerKind(object): + Invoked = 1 + TriggerCharacter = 2 + TriggerForIncompleteCompletions = 3 + + +class CompletionContext(object): + """ + Contains additional information about the context in which a completion request is triggered. + """ + def __init__(self, triggerKind, triggerCharacter = None): + """ + Constructs a new CompletionContext instance. + + :param CompletionTriggerKind triggerKind: How the completion was triggered. + :param str triggerCharacter: The trigger character (a single character) that has trigger code complete. + Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` + """ + self.triggerKind = triggerKind + if triggerCharacter: + self.triggerCharacter = triggerCharacter + + +class TextEdit(object): + """ + A textual edit applicable to a text document. + """ + def __init__(self, range, newText): + """ + :param Range range: The range of the text document to be manipulated. To insert + text into a document create a range where start === end. + :param str newText: The string to be inserted. For delete operations use an empty string. + """ + self.range = range + self.newText = newText + + +class InsertTextFormat(object): + PlainText = 1 + Snippet = 2 + + +class CompletionItem(object): + """ + """ + def __init__(self, label, \ + kind = None, \ + detail = None, \ + documentation = None, \ + deprecated = None, \ + presented = None, \ + sortText = None, \ + filterText = None, \ + insertText = None, \ + insertTextFormat = None, \ + textEdit = None, \ + additionalTextEdits = None, \ + commitCharacters = None, \ + command = None, \ + data = None, \ + score = 0.0 + ): + """ + :param str label: The label of this completion item. By default also the text that is inserted when selecting + this completion. + :param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor. + :param str detail: A human-readable string with additional information about this item, like type or symbol information. + :param tr ocumentation: A human-readable string that represents a doc-comment. + :param bool deprecated: Indicates if this item is deprecated. + :param bool presented: Select this item when showing. Note: that only one completion item can be selected and that the + tool / client decides which item that is. The rule is that the first item of those that match best is selected. + :param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used. + :param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used. + :param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used. + The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example + VS Code when code complete is requested in this example `con` and a completion item with an `insertText` of `console` is provided it + will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation. + @deprecated Use textEdit instead. + :param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property + and the `newText` property of a provided `textEdit`. + :param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored. + Note:* The range of the edit must be a single line range and it must contain the position at which completion + has been requested. + :param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion. + Edits must not overlap (including the same insert position) with the main edit nor with themselves. + Additional text edits should be used to change text unrelated to the current cursor position + (for example adding an import statement at the top of the file if the completion item will + insert an unqualified type). + :param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and + then type that character. *Note* that all commit characters should have `length=1` and that superfluous + characters will be ignored. + :param Command command: An optional command that is executed *after* inserting this completion. Note: that + additional modifications to the current document should be described with the additionalTextEdits-property. + :param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request. + :param float score: Score of the code completion item. + """ + self.label = label + self.kind = kind + self.detail = detail + self.documentation = documentation + self.deprecated = deprecated + self.presented = presented + self.sortText = sortText + self.filterText = filterText + self.insertText = insertText + self.insertTextFormat = insertTextFormat + self.textEdit = textEdit + self.additionalTextEdits = additionalTextEdits + self.commitCharacters = commitCharacters + self.command = command + self.data = data + self.score = score + + +class CompletionItemKind(enum.Enum): + Text = 1 + Method = 2 + Function = 3 + Constructor = 4 + Field = 5 + Variable = 6 + Class = 7 + Interface = 8 + Module = 9 + Property = 10 + Unit = 11 + Value = 12 + Enum = 13 + Keyword = 14 + Snippet = 15 + Color = 16 + File = 17 + Reference = 18 + Folder = 19 + EnumMember = 20 + Constant = 21 + Struct = 22 + Event = 23 + Operator = 24 + TypeParameter = 25 + + +class CompletionList(object): + """ + Represents a collection of [completion items](#CompletionItem) to be presented in the editor. + """ + def __init__(self, isIncomplete, items): + """ + Constructs a new CompletionContext instance. + + :param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list. + :param CompletionItem items: The completion items. + """ + self.isIncomplete = isIncomplete + self.items = [to_type(i, CompletionItem) for i in items] + +class ErrorCodes(enum.Enum): + # Defined by JSON RPC + ParseError = -32700 + InvalidRequest = -32600 + MethodNotFound = -32601 + InvalidParams = -32602 + InternalError = -32603 + serverErrorStart = -32099 + serverErrorEnd = -32000 + ServerNotInitialized = -32002 + UnknownErrorCode = -32001 + + # Defined by the protocol. + RequestCancelled = -32800 + ContentModified = -32801 + +class ResponseError(Exception): + def __init__(self, code, message, data = None): + self.code = code + self.message = message + if data: + self.data = data \ No newline at end of file