Moved lsp manager to lsp client

This commit is contained in:
itdominator 2024-09-16 23:30:22 -05:00
parent 3617e79150
commit 4d65197845
17 changed files with 207 additions and 1788 deletions

View File

@ -1,201 +0,0 @@
# 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
}
}

View File

@ -1,4 +1,5 @@
{
"lsp_manager_start_command": ["python", "/opt/lsp-manager.zip"],
"websocket": {
"host": "localhost",
"port": 8765

View File

@ -1,187 +0,0 @@
# 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.daemon = True
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, lsp_servers_config = {}):
super().__init__()
self.lsp_servers_config = lsp_servers_config
self.lsp_clients = {}
def _blame(self, response):
for d in response['diagnostics']:
if d['severity'] == 1:
print(f"An error occurs in {response['uri']} at {d['range']}:")
print(f"\t[{d['source']}] {d['message']}")
def _shutting_down(self):
keys = self.lsp_clients.keys()
for key in keys:
print(f"LSP Server: ( {key} ) Shutting Down...")
self.lsp_clients[key].shutdown()
self.lsp_clients[key].exit()
def _generate_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)
callbacks = {
"window/showMessage": print,
"textDocument/symbolStatus": print,
"textDocument/publishDiagnostics": self._blame,
}
lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint, notify_callbacks = callbacks)
lsp_client = pylspclient.LspClient(lsp_endpoint)
self.lsp_clients[language] = lsp_client
return lsp_client
def create_client(self, language = "", server_proc = None, initialization_options = None):
if not language or not server_proc: return False
root_path = None
# root_uri = 'file:///home/abaddon/Coding/Projects/Active/Python_Projects/000_Usable/gtk/Newton_Editor/src/'
# workspace_folders = [{'name': 'python-lsp', 'uri': root_uri}]
root_uri = ''
workspace_folders = [{'name': '', 'uri': root_uri}]
lsp_client = self._generate_client(language, server_proc)
lsp_client.initialize(
processId = server_proc.pid, \
rootPath = root_path, \
rootUri = root_uri, \
initializationOptions = initialization_options, \
capabilities = Capabilities.data, \
trace = "off", \
# trace = "on", \
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 do_open(self, language_id, uri):
if language_id in self.lsp_clients.keys():
lsp_client = self.lsp_clients[language_id]
else:
lsp_client = self.load_lsp_server(language_id)
if lsp_client:
self.register_opened_file(language_id, uri, lsp_client)
def do_save(self, language_id, uri):
if language_id in self.lsp_clients.keys():
self.lsp_clients[language_id].didSave(
pylspclient.lsp_structs.TextDocumentIdentifier(uri)
)
def do_close(self, language_id, uri):
if language_id in self.lsp_clients.keys():
self.lsp_clients[language_id].didClose(
pylspclient.lsp_structs.TextDocumentIdentifier(uri)
)
def do_goto(self, language_id, uri, line, offset):
if language_id in self.lsp_clients.keys():
return self.lsp_clients[language_id].definition(
pylspclient.lsp_structs.TextDocumentIdentifier(uri),
pylspclient.lsp_structs.Position(line, offset)
)
return []
def do_change(self, uri, language_id, line, start, end, text):
if language_id in self.lsp_clients.keys():
start_pos = pylspclient.lsp_structs.Position(line, start.get_line_offset())
end_pos = pylspclient.lsp_structs.Position(line, end.get_line_offset())
range_info = pylspclient.lsp_structs.Range(start_pos, end_pos)
text_length = len(text)
text_document = pylspclient.lsp_structs.TextDocumentItem(uri, language_id, 1, text)
change_event = pylspclient.lsp_structs.TextDocumentContentChangeEvent(range_info, text_length, text)
return self.lsp_clients[language_id].didChange( text_document, change_event )
return []
def do_completion(self, language_id, uri, line, offset, _char, is_invoked = False):
if language_id in self.lsp_clients.keys():
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerCharacter
if _char in [".", " "]:
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerCharacter
elif is_invoked:
trigger = pylspclient.lsp_structs.CompletionTriggerKind.Invoked
else:
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerForIncompleteCompletions
return self.lsp_clients[language_id].completion(
pylspclient.lsp_structs.TextDocumentIdentifier(uri),
pylspclient.lsp_structs.Position(line, offset),
None
# pylspclient.lsp_structs.CompletionContext(trigger, _char)
)
return []
def load_lsp_server(self, language_id):
if not language_id in self.lsp_servers_config.keys():
return
command = self.lsp_servers_config[language_id]["command"]
config_options = self.lsp_servers_config[language_id]["initialization_options"]
if command:
server_proc = self.create_lsp_server(command)
if self.create_client(language_id, server_proc, config_options):
return self.lsp_clients[language_id]
return None
def register_opened_file(self, language_id = "", uri = "", lsp_client = None):
if not language_id or not uri: return
text = open(uri[7:], "r").read()
version = 1
lsp_client.didOpen(
pylspclient.lsp_structs.TextDocumentItem(uri, language_id, version, text)
)

View File

@ -3,10 +3,16 @@
"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",
"credit": "",
"support": "",
"requests": {
"pass_events": "true"
"pass_events": "true",
"pass_ui_objects": [
"separator_right"
],
"bind_keys": [
"LSP Client Toggle||tggl_lsp_window:<Control>l"
]
}
}
}

View File

@ -1,72 +1,183 @@
# Python imports
import os
import signal
import subprocess
import json
import threading
# Lib imports
from gi.repository import GLib
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from plugins.plugin_base import PluginBase
from .lsp_controller import LSPController
class LSPPliginException(Exception):
...
from .client_ipc import ClientIPC
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_config_path: str = os.path.dirname(os.path.realpath(__file__)) + "/../../lsp_servers_config.json"
self.lsp_servers_config: dict = {}
self.lsp_controller = None
self.timer = None
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.config_file = "config.json"
self.config: dict = {}
self.lsp_client_proc = None
self.lsp_window = None
def generate_reference_ui_element(self):
...
def run(self):
if os.path.exists(self.lsp_config_path):
with open(self.lsp_config_path, "r") as f:
self.lsp_servers_config = json.load(f)
else:
text = f"LSP NOT Enabled.\nFile:\n\t{self.lsp_config_path}\ndoes no exsist..."
self._event_system.emit("bubble_message", ("warning", self.name, text,))
return
try:
with open(self.config_file) as f:
self.config = json.load(f)
except Exception as e:
raise Exception(f"Couldn't load config.json...\n{repr(e)}")
self.lsp_window = Gtk.Window()
box1 = Gtk.Box()
box2 = Gtk.Box()
start_btn = Gtk.Button(label = "Start LSP Client")
stop_btn = Gtk.Button(label = "Stop LSP Client")
pid_label = Gtk.Label(label = "LSP PID: ")
box1.set_orientation( Gtk.Orientation.VERTICAL )
self.lsp_window.set_deletable(False)
self.lsp_window.set_skip_pager_hint(True)
self.lsp_window.set_skip_taskbar_hint(True)
self.lsp_window.set_title("LSP Manager")
self.lsp_window.set_size_request(480, 320)
start_btn.connect("clicked", self.start_lsp_manager)
stop_btn.connect("clicked", self.stop_lsp_manager)
box1.add(pid_label)
box2.add(start_btn)
box2.add(stop_btn)
box1.add(box2)
self.lsp_window.add(box1)
box1.show_all()
self.inner_subscribe_to_events()
def _shutting_down(self):
self.stop_lsp_manager()
def _tear_down(self, widget, eve):
return True
def _tggl_lsp_window(self, widget = None):
if not self.lsp_window.is_visible():
self.lsp_window.show()
else:
self.lsp_window.hide()
if len(self.lsp_servers_config.keys()) > 0:
self.lsp_controller = LSPController(self.lsp_servers_config)
self.inner_subscribe_to_events()
def subscribe_to_events(self):
...
self._event_system.subscribe("tggl_lsp_window", self._tggl_lsp_window)
def inner_subscribe_to_events(self):
self._event_system.subscribe("shutting_down", self._shutting_down)
# self._event_system.subscribe("buffer_changed", self._buffer_changed)
self._event_system.subscribe("textDocument/didChange", self._buffer_changed)
self._event_system.subscribe("textDocument/didOpen", self.lsp_controller.do_open)
self._event_system.subscribe("textDocument/didSave", self.lsp_controller.do_save)
self._event_system.subscribe("textDocument/didClose", self.lsp_controller.do_close)
self._event_system.subscribe("textDocument/definition", self._do_goto)
self._event_system.subscribe("textDocument/completion", self._do_completion)
self._event_system.subscribe("textDocument/didOpen", self._lsp_did_open)
self._event_system.subscribe("textDocument/didSave", self._lsp_did_save)
self._event_system.subscribe("textDocument/didClose", self._lsp_did_close)
self._event_system.subscribe("textDocument/didChange", self._lsp_did_change)
self._event_system.subscribe("textDocument/definition", self._lsp_goto)
self._event_system.subscribe("textDocument/completion", self._lsp_completion)
def _shutting_down(self):
self.lsp_controller._shutting_down()
def start_lsp_manager(self, button):
if self.lsp_client_proc: return
self.lsp_client_proc = subprocess.Popen(self.config["lsp_manager_start_command"])
self._load_client_ipc_server()
def _load_client_ipc_server(self):
self.client_ipc = ClientIPC()
self.client_ipc.set_event_system(self._event_system)
self._ipc_realization_check(self.client_ipc)
if not self.client_ipc.is_ipc_alive:
raise AppLaunchException(f"LSP IPC Server Already Exists...")
def _ipc_realization_check(self, ipc_server):
try:
ipc_server.create_ipc_listener()
except Exception:
ipc_server.send_test_ipc_message()
try:
ipc_server.create_ipc_listener()
except Exception as e:
...
def stop_lsp_manager(self, button = None):
if not self.lsp_client_proc: return
if not self.lsp_client_proc.poll() is None:
self.lsp_client_proc = None
return
self.lsp_client_proc.terminate()
self.client_ipc.is_ipc_alive = False
self.lsp_client_proc = None
def _lsp_did_open(self, language_id: str, uri: str, text: str):
if not self.lsp_client_proc: return
data = {
"method": "textDocument/didOpen",
"language_id": language_id,
"uri": uri,
"version": -1,
"text": text,
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_save(self, uri: str, text: str):
if not self.lsp_client_proc: return
data = {
"method": "textDocument/didSave",
"language_id": "",
"uri": uri,
"version": -1,
"text": text,
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_close(self, uri: str):
if not self.lsp_client_proc: return
data = {
"method": "textDocument/didClose",
"language_id": "",
"uri": uri,
"version": -1,
"text": "",
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_change(self, language_id: str, uri: str, buffer):
if not self.lsp_client_proc: return
def _buffer_changed(self, file_type, buffer):
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
start = iter.copy()
end = iter.copy()
@ -75,12 +186,39 @@ class Plugin(PluginBase):
end.forward_to_line_end()
text = buffer.get_text(start, end, include_hidden_chars = False)
result = self.lsp_controller.do_change(buffer.uri, buffer.language_id, line, start, end, text)
data = {
"method": "textDocument/didChange",
"language_id": language_id,
"uri": uri,
"version": buffer.version_id,
"text": text,
"line": line,
"column": column,
"char": ""
}
self.send_message(data)
def _lsp_goto(self, language_id: str, uri: str, line: int, column: int):
if not self.lsp_client_proc: return
data = {
"method": "textDocument/definition",
"language_id": language_id,
"uri": uri,
"version": -1,
"text": "",
"line": line,
"column": column,
"char": ""
}
self.send_message(data)
def _lsp_completion(self, source_view):
if not self.lsp_client_proc: return
def _do_completion(self, source_view):
filepath = source_view.get_current_file()
if not filepath: return
uri = filepath.get_uri()
@ -88,27 +226,23 @@ class Plugin(PluginBase):
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
_char = iter.get_char()
char = iter.get_char()
if iter.backward_char():
_char = iter.get_char()
char = iter.get_char()
offset = iter.get_line_offset()
result = self.lsp_controller.do_completion(
source_view.get_filetype(),
uri,
line,
offset,
_char
)
column = iter.get_line_offset()
data = {
"method": "textDocument/completion",
"language_id": source_view.get_filetype(),
"uri": uri,
"version": source_view.get_version_id(),
"text": "",
"line": line,
"column": column,
"char": char
}
return result
self.send_message(data)
def _do_goto(self, language_id, uri, line, offset):
results = self.lsp_controller.do_goto(language_id, uri, line, offset)
if len(results) == 1:
result = results[0]
file = result.uri[7:]
line = result.range.end.line
message = f"FILE|{file}:{line}"
self._event_system.emit("post_file_to_ipc", message)
def send_message(self, data: dict):
self.client_ipc.send_manager_ipc_message( json.dumps(data) )

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,6 +0,0 @@
__all__ = []
from .json_rpc_endpoint import JsonRpcEndpoint
from .lsp_client import LspClient
from .lsp_endpoint import LspEndpoint
from . import lsp_structs

View File

@ -1,105 +0,0 @@
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)

View File

@ -1,257 +0,0 @@
# Python imports
# Lib imports
# Application imports
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):
"""
"""
return self.lsp_endpoint.call_method("shutdown")
def exit(self):
"""
"""
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 didSave(self, textDocument):
"""
:param TextDocumentIdentifier textDocument: The document that was saved.
"""
return self.lsp_endpoint.send_notification("textDocument/didSave", textDocument = textDocument)
def didClose(self, textDocument):
"""
:param TextDocumentIdentifier textDocument: The document that was closed.
"""
return self.lsp_endpoint.send_notification("textDocument/didClose", 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 TextDocumentItem textDocument: The text document.
: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 )
if not result_dict: return []
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 not result_dict: return []
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
)
if not result_dict: return []
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/typeDefinition",
textDocument = textDocument,
position = position
)
if not result_dict: return []
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
)
if not result_dict: return []
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 not result_dict: return []
if "isIncomplete" in result_dict:
return lsp_structs.CompletionList(**result_dict)
return [lsp_structs.CompletionItem(**loc) for loc 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)
if not result_dict: return []
return [lsp_structs.Location(**loc) for loc in result_dict]

View File

@ -1,111 +0,0 @@
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: 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 is not None:
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)

View File

@ -1,566 +0,0 @@
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, \
preselect = 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 preselect: 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<cursor position>` 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.preselect = preselect
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 preselect 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

View File

@ -1,3 +0,0 @@
"""
Pligin Module
"""

View File

@ -1,3 +0,0 @@
"""
Pligin Package
"""

View File

@ -1,14 +0,0 @@
{
"manifest": {
"name": "LSP Manager",
"author": "ITDominator",
"version": "0.0.1",
"credit": "",
"support": "",
"requests": {
"pass_events": "true",
"pass_ui_objects": ["separator_right"],
"bind_keys": ["LSP Manager Toggle||tggl_lsp_window:<Control>l"]
}
}
}

View File

@ -1,248 +0,0 @@
# Python imports
import signal
import subprocess
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from plugins.plugin_base import PluginBase
from .client_ipc import ClientIPC
class Plugin(PluginBase):
def __init__(self):
super().__init__()
self.name = "LSP Manager" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
# where self.name should not be needed for message comms
self.ws_config_file = "config.json"
self.ws_config: dict = {}
self.lsp_manager_proc = None
self.lsp_window = None
def generate_reference_ui_element(self):
...
def run(self):
try:
with open(self.ws_config_file) as f:
self.ws_config = json.load(f)["websocket"]
except Exception as e:
raise Exception(f"Couldn't load config.json...\n{repr(e)}")
self.lsp_window = Gtk.Window()
box1 = Gtk.Box()
box2 = Gtk.Box()
start_btn = Gtk.Button(label = "Start LSP Manager")
stop_btn = Gtk.Button(label = "Stop LSP Manager")
pid_label = Gtk.Label(label = "LSP PID: ")
box1.set_orientation( Gtk.Orientation.VERTICAL )
self.lsp_window.set_deletable(False)
self.lsp_window.set_skip_pager_hint(True)
self.lsp_window.set_skip_taskbar_hint(True)
self.lsp_window.set_title("LSP Manager")
self.lsp_window.set_size_request(480, 320)
start_btn.connect("clicked", self.start_lsp_manager)
stop_btn.connect("clicked", self.stop_lsp_manager)
box1.add(pid_label)
box2.add(start_btn)
box2.add(stop_btn)
box1.add(box2)
self.lsp_window.add(box1)
box1.show_all()
self.inner_subscribe_to_events()
def _shutting_down(self):
self.stop_lsp_manager()
def _tear_down(self, widget, eve):
return True
def _tggl_lsp_window(self):
if not self.lsp_window.is_visible():
self.lsp_window.show()
else:
self.lsp_window.hide()
def subscribe_to_events(self):
self._event_system.subscribe("tggl_lsp_window", self._tggl_lsp_window)
def inner_subscribe_to_events(self):
self._event_system.subscribe("shutting_down", self._shutting_down)
self._event_system.subscribe("textDocument/didOpen", self._lsp_did_open)
self._event_system.subscribe("textDocument/didSave", self._lsp_did_save)
self._event_system.subscribe("textDocument/didClose", self._lsp_did_close)
self._event_system.subscribe("textDocument/didChange", self._lsp_did_change)
self._event_system.subscribe("textDocument/definition", self._lsp_goto)
self._event_system.subscribe("textDocument/completion", self._lsp_completion)
def start_lsp_manager(self, button):
if self.lsp_manager_proc: return
self.lsp_manager_proc = subprocess.Popen(["python", "/opt/lsp-manager.zip"])
# self.lsp_manager_proc = subprocess.Popen(["lsp-manager"])
self._load_client_ipc_server()
def _load_client_ipc_server(self):
self.client_ipc = ClientIPC()
self.client_ipc.set_event_system(self._event_system)
self._ipc_realization_check(self.client_ipc)
if not self.client_ipc.is_ipc_alive:
raise AppLaunchException(f"LSP IPC Server Already Exists...")
def _ipc_realization_check(self, ipc_server):
try:
ipc_server.create_ipc_listener()
except Exception:
ipc_server.send_test_ipc_message()
try:
ipc_server.create_ipc_listener()
except Exception as e:
...
def stop_lsp_manager(self, button = None):
if not self.lsp_manager_proc: return
if not self.lsp_manager_proc.poll() is None:
self.lsp_manager_proc = None
return
self.lsp_manager_proc.terminate()
self.client_ipc.is_ipc_alive = False
self.lsp_manager_proc = None
def _lsp_did_open(self, language_id: str, uri: str, text: str):
if not self.lsp_manager_proc: return
data = {
"method": "textDocument/didOpen",
"language_id": language_id,
"uri": uri,
"version": -1,
"text": text,
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_save(self, uri: str, text: str):
if not self.lsp_manager_proc: return
data = {
"method": "textDocument/didSave",
"language_id": "",
"uri": uri,
"version": -1,
"text": text,
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_close(self, uri: str):
if not self.lsp_manager_proc: return
data = {
"method": "textDocument/didClose",
"language_id": "",
"uri": uri,
"version": -1,
"text": "",
"line": -1,
"column": -1,
"char": ""
}
self.send_message(data)
def _lsp_did_change(self, language_id: str, uri: str, buffer):
if not self.lsp_manager_proc: return
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
start = iter.copy()
end = iter.copy()
start.backward_line()
start.forward_line()
end.forward_to_line_end()
text = buffer.get_text(start, end, include_hidden_chars = False)
data = {
"method": "textDocument/didChange",
"language_id": language_id,
"uri": uri,
"version": buffer.version_id,
"text": text,
"line": line,
"column": column,
"char": ""
}
self.send_message(data)
def _lsp_goto(self, language_id: str, uri: str, line: int, column: int):
if not self.lsp_manager_proc: return
data = {
"method": "textDocument/definition",
"language_id": language_id,
"uri": uri,
"version": -1,
"text": "",
"line": line,
"column": column,
"char": ""
}
self.send_message(data)
def _lsp_completion(self, source_view):
if not self.lsp_manager_proc: return
filepath = source_view.get_current_file()
if not filepath: return
uri = filepath.get_uri()
buffer = source_view.get_buffer()
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
char = iter.get_char()
if iter.backward_char():
char = iter.get_char()
column = iter.get_line_offset()
data = {
"method": "textDocument/completion",
"language_id": source_view.get_filetype(),
"uri": uri,
"version": source_view.get_version_id(),
"text": "",
"line": line,
"column": column,
"char": char
}
self.send_message(data)
def send_message(self, data: dict):
self.client_ipc.send_manager_ipc_message( json.dumps(data) )