moved classes to different files and fixed __init__
This commit is contained in:
parent
382b023ba5
commit
ac17709700
145
examples/clangd.py
Normal file
145
examples/clangd.py
Normal file
@ -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()
|
@ -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()
|
|
8
pylspclient/__init__.py
Normal file
8
pylspclient/__init__.py
Normal file
@ -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
|
72
pylspclient/json_rpc_endpoint.py
Normal file
72
pylspclient/json_rpc_endpoint.py
Normal file
@ -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)
|
125
pylspclient/lsp_client.py
Normal file
125
pylspclient/lsp_client.py
Normal file
@ -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)
|
75
pylspclient/lsp_endpoint.py
Normal file
75
pylspclient/lsp_endpoint.py
Normal file
@ -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)
|
2
setup.py
2
setup.py
@ -22,6 +22,6 @@ class PyTest(TestCommand):
|
|||||||
setup(
|
setup(
|
||||||
name="pylspclient",
|
name="pylspclient",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
tests_require=["pytest"],
|
tests_require=["pytest", "pytest_mock"],
|
||||||
cmdclass={"test": PyTest},
|
cmdclass={"test": PyTest},
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from lsp import json_rpc_client
|
import pylspclient
|
||||||
|
from pytest_mock import mocker
|
||||||
|
|
||||||
# content of test_sample.py
|
# content of test_sample.py
|
||||||
def func(x):
|
def func(x):
|
||||||
return x + 1
|
return x + 1
|
||||||
|
Loading…
Reference in New Issue
Block a user