Compare commits
11 Commits
49d673b0b8
...
master
Author | SHA1 | Date | |
---|---|---|---|
198f28d994 | |||
905e034137 | |||
8792bd45fc | |||
bb8bd19f64 | |||
9af1594e32 | |||
91236b728b | |||
4639a79c00 | |||
f144963c32 | |||
7a346a9185 | |||
592bd8c628 | |||
cfea3b5f11 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.egg-info
|
||||
__pycache__
|
||||
*.pyc
|
70
java
70
java
@@ -1,70 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
FILE_URI = "file://"
|
||||
|
||||
def locate_product_directory (jdt_ls_path):
|
||||
return os.path.join(jdt_ls_path, "org.eclipse.jdt.ls.product/target/repository")
|
||||
|
||||
def locate_launcher_jar (jdt_ls_path):
|
||||
jdt_ls_product_directory = locate_product_directory(jdt_ls_path)
|
||||
plugins_directory = os.path.join(jdt_ls_product_directory, "plugins")
|
||||
return os.path.join(plugins_directory, [jar for jar in os.listdir(plugins_directory) if jar.startswith("org.eclipse.equinox.launcher_")][0])
|
||||
|
||||
def locate_launcher_configuration (jdt_ls_path):
|
||||
return os.path.join(locate_product_directory(jdt_ls_path), "config_linux")
|
||||
|
||||
# TODO
|
||||
def locate_lombok ():
|
||||
return "/home/malacoda/.m2/repository/org/projectlombok/lombok/1.18.16/lombok-1.18.16.jar"
|
||||
|
||||
class Java(LspServer):
|
||||
def __init__ (self, arguments):
|
||||
self.path = arguments[0]
|
||||
super().__init__([
|
||||
"java",
|
||||
f"-javaagent:{locate_lombok()}",
|
||||
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044",
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-noverify",
|
||||
"-Xmx1G",
|
||||
"-jar", locate_launcher_jar(self.path),
|
||||
"-configuration", locate_launcher_configuration(self.path),
|
||||
"-data", os.path.join(self.path, "data"),
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens", "java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
||||
] + arguments[1:])
|
||||
|
||||
@command("java.apply.workspaceEdit")
|
||||
def workspaceEdit (self, arguments):
|
||||
print(f"we {arguments}", file=sys.stderr)
|
||||
for argument in arguments:
|
||||
for file_name, changes in argument['changes'].items():
|
||||
file_name = file_name[len(FILE_URI):]
|
||||
file_contents = ""
|
||||
|
||||
if os.path.exists(file_name):
|
||||
with open(file_name) as in_file:
|
||||
file_contents = in_file.read()
|
||||
|
||||
for change in changes:
|
||||
file_contents = perform_change(file_contents, change)
|
||||
|
||||
with open(file_name, "w") as out_file:
|
||||
out_file.write(file_contents)
|
||||
|
||||
def get_character_offset (file_contents, line, character):
|
||||
line_pos = 0
|
||||
while line > 0:
|
||||
line_pos = file_contents.index("\n", line_pos) + 1
|
||||
line = line - 1
|
||||
return line_pos + character
|
||||
|
||||
def perform_change (file_text, change):
|
||||
change_start = get_character_offset(file_text, change['range']['start']['line'], change['range']['start']['character'])
|
||||
change_end = get_character_offset(file_text, change['range']['end']['line'], change['range']['end']['character'])
|
||||
print(f"change {change_start} {change['newText']} {change_end}", file=sys.stderr)
|
||||
return file_text[:change_start] + change['newText'] + file_text[change_end:]
|
135
lsp-proxy
135
lsp-proxy
@@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import json
|
||||
|
||||
from subprocess import run, Popen, PIPE
|
||||
from threading import Thread
|
||||
|
||||
CONTENT_LENGTH = "Content-Length: "
|
||||
|
||||
def process_messages (source, sink, handlers, log):
|
||||
print("start message handler", file=log)
|
||||
while True:
|
||||
line = source.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.decode()
|
||||
if line.startswith(CONTENT_LENGTH):
|
||||
content_length = int(line[len(CONTENT_LENGTH):].strip())
|
||||
print(f">> ce: [{content_length}]", file=log)
|
||||
|
||||
source.readline()
|
||||
|
||||
payload = source.read(content_length).decode()
|
||||
print(f">> payload: [{payload}]", file=log)
|
||||
|
||||
if handlers:
|
||||
payload_parsed = json.loads(payload)
|
||||
for handler in handlers:
|
||||
try:
|
||||
handler(payload_parsed)
|
||||
except Exception as e:
|
||||
print(f"Error from handler: {e}", file=log)
|
||||
|
||||
transmit_payload(payload, sink)
|
||||
|
||||
print("stop message handler", file=log)
|
||||
|
||||
def transmit_payload (payload, sink):
|
||||
sink.write(f"{CONTENT_LENGTH}{len(payload)}\r\n\r\n{payload}".encode())
|
||||
sink.flush()
|
||||
|
||||
class Handlers:
|
||||
def __init__ (self):
|
||||
self.handlers = []
|
||||
|
||||
def __call__ (self, func):
|
||||
self.handlers.append(func)
|
||||
return func
|
||||
|
||||
def get_handlers (self, server):
|
||||
return [lambda payload: handler(server, payload) for handler in self.handlers]
|
||||
|
||||
handler = Handlers()
|
||||
|
||||
def method (name):
|
||||
def method_handler (func):
|
||||
def handle_method (server, payload):
|
||||
if payload.get("method", None) != name:
|
||||
return
|
||||
func(server, payload.get("params"))
|
||||
return handler(handle_method)
|
||||
return method_handler
|
||||
|
||||
def command (name):
|
||||
def command_handler (func):
|
||||
def handle_command (server, payload):
|
||||
if payload.get("command", None) != name:
|
||||
return
|
||||
func(server, payload.get("arguments"))
|
||||
return method("workspace/executeCommand")(handle_command)
|
||||
return command_handler
|
||||
|
||||
class LspServer:
|
||||
def __init__ (self, command):
|
||||
self.command = command
|
||||
self.handlers = []
|
||||
|
||||
def start (self):
|
||||
handlers = self.handlers + handler.get_handlers(self)
|
||||
|
||||
with Popen(self.command, stdin=PIPE, stdout=PIPE) as process:
|
||||
self.process = process
|
||||
|
||||
self.process_reader = Thread(target=process_messages, args=[process.stdout, sys.stdout.buffer, handlers, sys.stderr])
|
||||
self.process_reader.start()
|
||||
|
||||
process_messages(sys.stdin.buffer, process.stdin, handlers, sys.stderr)
|
||||
|
||||
def start2 (self):
|
||||
run(self.command)
|
||||
|
||||
def send_to_client (self, payload):
|
||||
transmit_payload(sys.stdout, payload)
|
||||
|
||||
def send_to_server (self, payload):
|
||||
transmit_payload(self.process.stdout, payload)
|
||||
|
||||
def close (self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
|
||||
def find_server_classes (values=None):
|
||||
values = values or globals()
|
||||
return dict([item for item in values.items() if inspect.isclass(item[1]) and LspServer in inspect.getmro(item[1])])
|
||||
|
||||
def select_server_class (argument):
|
||||
classes = find_server_classes()
|
||||
if argument in classes:
|
||||
return classes[argument]
|
||||
|
||||
if os.path.isfile(argument):
|
||||
script_locals = {
|
||||
"LspServer": LspServer,
|
||||
"handler": handler,
|
||||
"method": method,
|
||||
"command": command
|
||||
}
|
||||
|
||||
with open(argument) as server_script:
|
||||
exec(server_script.read(), script_locals)
|
||||
|
||||
return [item[1] for item in find_server_classes(script_locals).items() if not item[1] == LspServer][0]
|
||||
|
||||
server_class = select_server_class(sys.argv[1])
|
||||
server = server_class(sys.argv[2:])
|
||||
|
||||
try:
|
||||
server.start()
|
||||
except Exception as e:
|
||||
server.close()
|
||||
raise e
|
38
lsp_proxy/__init__.py
Executable file
38
lsp_proxy/__init__.py
Executable file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
from .lsp import LspServer, handler, method, command
|
||||
from .java import Java
|
||||
|
||||
def find_server_classes (values=None):
|
||||
values = values or globals()
|
||||
return dict([(item[0].lower(), item[1]) for item in values.items() if inspect.isclass(item[1]) and LspServer in inspect.getmro(item[1])])
|
||||
|
||||
def select_server_class (argument):
|
||||
classes = find_server_classes()
|
||||
if argument.lower() in classes:
|
||||
return classes[argument]
|
||||
|
||||
if os.path.isfile(argument):
|
||||
script_locals = {
|
||||
"LspServer": LspServer,
|
||||
"handler": handler,
|
||||
"method": method,
|
||||
"command": command
|
||||
}
|
||||
|
||||
with open(argument) as server_script:
|
||||
exec(server_script.read(), script_locals)
|
||||
|
||||
return [item[1] for item in find_server_classes(script_locals).items() if not item[1] == LspServer][0]
|
||||
|
||||
def main ():
|
||||
server_class = select_server_class(sys.argv[1])
|
||||
server = server_class(sys.argv[2:])
|
||||
|
||||
try:
|
||||
server.start()
|
||||
except Exception as e:
|
||||
server.close()
|
||||
raise e
|
128
lsp_proxy/java.py
Normal file
128
lsp_proxy/java.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import tarfile
|
||||
from urllib import request
|
||||
|
||||
from .lsp import LspServer, command, get_data_home
|
||||
|
||||
FILE_URI = "file://"
|
||||
|
||||
VERSION = "1.3.0"
|
||||
DOWNLOADS_URI = "http://download.eclipse.org/jdtls/milestones/{version}/{file}"
|
||||
LATEST = "latest.txt"
|
||||
|
||||
def locate_product_directory (jdt_ls_path):
|
||||
if os.path.isdir(os.path.join(jdt_ls_path, "plugins")):
|
||||
return jdt_ls_path
|
||||
|
||||
return os.path.join(jdt_ls_path, "org.eclipse.jdt.ls.product/target/repository")
|
||||
|
||||
def locate_launcher_jar (jdt_ls_path):
|
||||
plugins_directory = os.path.join(jdt_ls_path, "plugins")
|
||||
return os.path.join(plugins_directory, [jar for jar in os.listdir(plugins_directory) if jar.startswith("org.eclipse.equinox.launcher_")][0])
|
||||
|
||||
def locate_launcher_configuration (jdt_ls_path):
|
||||
return os.path.join(jdt_ls_path, "config_linux")
|
||||
|
||||
def log (message, *args, **kwargs):
|
||||
print(message.format(*args, **kwargs), file=sys.stderr)
|
||||
|
||||
def download_jdt_ls (version, destination):
|
||||
if not os.path.isdir(destination):
|
||||
os.makedirs(destination)
|
||||
|
||||
current_version = None
|
||||
current_version_file = os.path.join(destination, LATEST)
|
||||
if os.path.isfile(current_version_file):
|
||||
with open(current_version_file) as f:
|
||||
current_version = f.read().strip()
|
||||
log("Currently downloaded version is {version}:", version=current_version)
|
||||
|
||||
latest_version = None
|
||||
with request.urlopen(DOWNLOADS_URI.format(version=version, file=LATEST)) as f:
|
||||
latest_version = f.read().decode().strip()
|
||||
log("Latest available version is {version}:", version=latest_version)
|
||||
|
||||
if not latest_version == current_version:
|
||||
latest_uri = DOWNLOADS_URI.format(version=version, file=latest_version)
|
||||
log("Downloading latest version from: {uri}:", uri=latest_uri)
|
||||
with request.urlopen(latest_uri) as f:
|
||||
with tarfile.open(fileobj=f, mode="r|gz") as tar:
|
||||
tar.extractall(destination)
|
||||
print(tar)
|
||||
|
||||
with open(current_version_file, "w") as cvf:
|
||||
cvf.write(latest_version)
|
||||
|
||||
return destination
|
||||
|
||||
# TODO
|
||||
def locate_lombok ():
|
||||
return os.path.join(os.environ['HOME'], ".m2/repository/org/projectlombok/lombok/1.18.16/lombok-1.18.16.jar")
|
||||
|
||||
def get_argument (arguments, arg_name):
|
||||
if not arg_name in args:
|
||||
return
|
||||
|
||||
value = arguments.pop(arguments.index(arg_name) + 1)
|
||||
arguments.remove(arg_name)
|
||||
return value
|
||||
|
||||
class Java(LspServer):
|
||||
def __init__ (self, arguments):
|
||||
self.path = arguments[0] if arguments else None
|
||||
|
||||
if self.path is None or not os.path.exists(self.path):
|
||||
version = VERSION
|
||||
|
||||
if "--version" in arguments:
|
||||
version = get_argument(arguments, "--version")
|
||||
|
||||
self.path = download_jdt_ls(version, os.path.join(get_data_home(), "java", "jdt.ls", version))
|
||||
else:
|
||||
self.path = locate_product_directory(self.path)
|
||||
|
||||
command = ["java"]
|
||||
if "--enable-lombok" in arguments:
|
||||
arguments.remove("--enable-lombok")
|
||||
command.append(f"-javaagent:{locate_lombok()}")
|
||||
|
||||
data_directory = None
|
||||
if "--data" in arguments:
|
||||
data_directory = get_argument(arguments, "--data")
|
||||
else:
|
||||
data_directory = os.path.join(get_data_home(), "java", "data")
|
||||
|
||||
if "--enable-debugger" in arguments:
|
||||
arguments.remove("--enable-debugger")
|
||||
command.append("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044")
|
||||
|
||||
command = command + [
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-noverify",
|
||||
"-Xmx1G",
|
||||
"-jar", locate_launcher_jar(self.path),
|
||||
"-configuration", locate_launcher_configuration(self.path),
|
||||
"-data", data_directory,
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens", "java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
||||
] + arguments[1:]
|
||||
|
||||
super().__init__(command)
|
||||
|
||||
@command("java.apply.workspaceEdit")
|
||||
def workspaceEdit (self, arguments):
|
||||
for argument in arguments:
|
||||
self.send_to_client({
|
||||
"id": -1,
|
||||
"method": "workspace/applyEdit",
|
||||
"params": {
|
||||
"edit": {
|
||||
"changes": argument['changes']
|
||||
}
|
||||
}
|
||||
})
|
123
lsp_proxy/lsp.py
Normal file
123
lsp_proxy/lsp.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import json
|
||||
|
||||
from subprocess import run, Popen, PIPE
|
||||
from threading import Thread
|
||||
|
||||
from xdg import xdg_data_home
|
||||
|
||||
CONTENT_LENGTH = "Content-Length: "
|
||||
|
||||
def log (message, *args, **kwargs):
|
||||
print(message.format(*args, **kwargs), file=sys.stderr)
|
||||
|
||||
def process_messages (label, source, sink, handlers):
|
||||
log(f"[{label}] start process_messages")
|
||||
while True:
|
||||
line = source.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
try:
|
||||
line = line.decode()
|
||||
if line.startswith(CONTENT_LENGTH):
|
||||
content_length = int(line[len(CONTENT_LENGTH):].strip())
|
||||
log(f">> [{label}] ce: [{content_length}]")
|
||||
|
||||
source.readline()
|
||||
|
||||
payload = source.read(content_length).decode()
|
||||
log(">> [{label}] payload: [{payload}]", label=label, payload=payload)
|
||||
|
||||
if handlers:
|
||||
payload_parsed = json.loads(payload)
|
||||
for handler in handlers:
|
||||
try:
|
||||
handler(payload_parsed)
|
||||
except Exception as e:
|
||||
log(f"Error from handler: {e}")
|
||||
|
||||
transmit_payload(payload, sink)
|
||||
except Exception as e:
|
||||
log(f"[{label}] Error decoding input: {e}")
|
||||
|
||||
log(f"[{label}] exit process_messages")
|
||||
|
||||
def transmit_payload (payload, sink):
|
||||
if isinstance(payload, dict):
|
||||
payload = json.dumps(payload)
|
||||
|
||||
sink.write(f"{CONTENT_LENGTH}{len(payload)}\r\n\r\n{payload}".encode())
|
||||
sink.flush()
|
||||
|
||||
class Handlers:
|
||||
def __init__ (self):
|
||||
self.handlers = []
|
||||
|
||||
def __call__ (self, func):
|
||||
self.handlers.append(func)
|
||||
return func
|
||||
|
||||
def get_handlers (self, server):
|
||||
return [lambda payload: handler(server, payload) for handler in self.handlers]
|
||||
|
||||
handler = Handlers()
|
||||
|
||||
def method (name):
|
||||
def method_handler (func):
|
||||
def handle_method (server, payload):
|
||||
if payload.get("method", None) != name:
|
||||
return
|
||||
func(server, payload.get("params"))
|
||||
return handler(handle_method)
|
||||
return method_handler
|
||||
|
||||
def command (name):
|
||||
def command_handler (func):
|
||||
def handle_command (server, payload):
|
||||
if payload.get("command", None) != name:
|
||||
return
|
||||
func(server, payload.get("arguments"))
|
||||
return method("workspace/executeCommand")(handle_command)
|
||||
return command_handler
|
||||
|
||||
def get_data_home ():
|
||||
data_directory = os.path.join(xdg_data_home(), "lsp-proxy")
|
||||
if not os.path.exists(data_directory):
|
||||
os.makedirs(data_directory)
|
||||
return data_directory
|
||||
|
||||
class LspServer:
|
||||
def __init__ (self, command):
|
||||
self.command = command
|
||||
self.handlers = []
|
||||
|
||||
def start (self):
|
||||
handlers = self.handlers + handler.get_handlers(self)
|
||||
|
||||
log("starting lsp process: {}", " ".join(self.command))
|
||||
with Popen(self.command, stdin=PIPE, stdout=PIPE) as process:
|
||||
self.process = process
|
||||
|
||||
self.process_reader = Thread(target=process_messages, args=["process.stdout -> sys.stdout reader", process.stdout, sys.stdout.buffer, handlers])
|
||||
self.process_reader.start()
|
||||
|
||||
process_messages("sys.stdin -> process.stdin", sys.stdin.buffer, process.stdin, handlers)
|
||||
|
||||
log("lsp process ended")
|
||||
|
||||
def start2 (self):
|
||||
run(self.command)
|
||||
|
||||
def send_to_client (self, payload):
|
||||
transmit_payload(payload, sys.stdout.buffer)
|
||||
|
||||
def send_to_server (self, payload):
|
||||
transmit_payload(payload, self.process.stdout)
|
||||
|
||||
def close (self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
|
17
setup.py
Normal file
17
setup.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
setup(
|
||||
name='lsp-proxy',
|
||||
version='0.0.1',
|
||||
description='lsp-proxy is a proxy for language servers.',
|
||||
author='Adrian Malacoda',
|
||||
packages=['lsp_proxy'],
|
||||
install_requires=['xdg'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'lsp-proxy = lsp_proxy:main'
|
||||
]
|
||||
}
|
||||
)
|
Reference in New Issue
Block a user