develop #3

Merged
itdominator merged 69 commits from develop into master 2026-03-23 04:51:23 +00:00
317 changed files with 17912 additions and 881 deletions
Showing only changes of commit 52db0b8a31 - Show all commits

View File

@@ -1,5 +1,7 @@
# Python imports # Python imports
import threading import threading
from os import path
import json
# Lib imports # Lib imports
import gi import gi
@@ -20,17 +22,15 @@ class LSPController(LSPControllerWebsocket):
# initialize-params-slim.json was created off of jedi_language_server one # initialize-params-slim.json was created off of jedi_language_server one
# self._init_params = settings_manager.get_lsp_init_data() # self._init_params = settings_manager.get_lsp_init_data()
self._language: str = "" self._language: str = ""
self._init_params: dict = {} self._init_params: dict = {}
self._event_history: dict[str] = {} self._event_history: dict[int, str] = {}
try: try:
from os import path
import json
_USER_HOME = path.expanduser('~') _USER_HOME = path.expanduser('~')
_SCRIPT_PTH = path.dirname( path.realpath(__file__) ) _SCRIPT_PTH = path.dirname( path.realpath(__file__) )
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json" _LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
with open(_LSP_INIT_CONFIG) as file: with open(_LSP_INIT_CONFIG) as file:
data = file.read().replace("{user.home}", _USER_HOME) data = file.read().replace("{user.home}", _USER_HOME)
self._init_params = json.loads(data) self._init_params = json.loads(data)
@@ -42,7 +42,7 @@ class LSPController(LSPControllerWebsocket):
self.read_lock = threading.Lock() self.read_lock = threading.Lock()
self.write_lock = threading.Lock() self.write_lock = threading.Lock()
def set_language(self, language): def set_language(self, language: str):
self._language = language self._language = language
def set_socket(self, socket: str): def set_socket(self, socket: str):
@@ -51,15 +51,15 @@ class LSPController(LSPControllerWebsocket):
def unset_socket(self): def unset_socket(self):
self._socket = None self._socket = None
def send_notification(self, method: str, params: {} = {}): def send_notification(self, method: str, params: dict = {}):
self._send_message( ClientNotification(method, params) ) self._send_message( ClientNotification(method, params) )
def send_request(self, method: str, params: {} = {}): def send_request(self, method: str, params: dict = {}):
self._message_id += 1 self._message_id += 1
self._event_history[self._message_id] = method self._event_history[self._message_id] = method
self._send_message( ClientRequest(self._message_id, method, params) ) self._send_message( ClientRequest(self._message_id, method, params) )
def get_event_by_id(self, message_id: int): def get_event_by_id(self, message_id: int) -> str:
if not message_id in self._event_history: return if not message_id in self._event_history: return
return self._event_history[message_id] return self._event_history[message_id]

View File

@@ -1,15 +1,14 @@
# Python imports # Python imports
import traceback
import subprocess
# Lib imports # Lib imports
from gi.repository import GLib from gi.repository import GLib
# Application imports # Application imports
# from libs import websockets # from libs import websockets
from libs.dto.code.lsp.lsp_messages import LEN_HEADER, TYPE_HEADER, get_message_str, get_message_obj from libs.dto.code.lsp.lsp_messages import get_message_str, get_message_obj
from libs.dto.code.lsp.lsp_message_structs import \ from libs.dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification LSPResponseTypes, ClientRequest, ClientNotification, \
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from .lsp_controller_base import LSPControllerBase from .lsp_controller_base import LSPControllerBase
from .websocket_client import WebsocketClient from .websocket_client import WebsocketClient
@@ -17,7 +16,7 @@ from .websocket_client import WebsocketClient
class LSPControllerWebsocket(LSPControllerBase): class LSPControllerWebsocket(LSPControllerBase):
def _send_message(self, data: ClientRequest or ClientNotification): def _send_message(self, data: ClientRequest | ClientNotification):
if not data: return if not data: return
message_str = get_message_str(data) message_str = get_message_str(data)
@@ -39,7 +38,7 @@ class LSPControllerWebsocket(LSPControllerBase):
if not hasattr(self, "ws_client"): return if not hasattr(self, "ws_client"): return
self.ws_client.close_client() self.ws_client.close_client()
def _monitor_lsp_response(self, data: None or {}): def _monitor_lsp_response(self, data: dict | None):
if not data: return if not data: return
message = get_message_obj(data) message = get_message_obj(data)

View File

@@ -0,0 +1,5 @@
from .base import BaseHandler
from .default import DefaultHandler
from .python import PythonHandler
from .java import JavaHandler
from .registry import HandlerRegistry

View File

@@ -0,0 +1,31 @@
# Python imports
from abc import ABC
# Lib imports
# Application imports
class BaseHandler:
def __init__(self):
self.context = None
self.response_cache = None
def set_context(self, context):
self.context = context
def set_response_cache(self, response_cache):
self.response_cache = response_cache
@property
def emit(self):
return self.context.emit
@property
def emit_to(self):
return self.context.emit_to
def handle(self, method: str, response, controller):
pass

View File

@@ -0,0 +1,100 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .base import BaseHandler
class DefaultHandler(BaseHandler):
"""Fallback handler for unknown languages - uses generic LSP handling."""
def handle(self, method: str, response, controller):
match method:
case "textDocument/completion":
self._handle_completion(response)
case "textDocument/definition":
self._handle_definition(response, controller)
case "textDocument/publishDiagnostics":
self._handle_diagnostics(response)
def _handle_completion(self, result):
if not result: return
items = result.get("items", []) if isinstance(result, dict) else result
self.response_cache.matchers.clear()
for item in items:
label = item.get("label")
if not label:
continue
text = (
item.get("insertText")
or item.get("textEdit", {}).get("newText")
or item.get("textEditText", "")
or label
)
detail = item.get("detail")
doc = item.get("documentation")
if detail:
info = detail
elif isinstance(doc, dict):
info = doc.get("value", "")
else:
info = str(doc) if doc else ""
self.response_cache.matchers[label] = {
"label": label,
"text": text,
"info": info,
}
self.context._prompt_completion_request()
def _handle_definition(self, response, controller):
if not response: return
uri = response[0]["uri"]
self.context._prompt_goto_request(uri, response[0]["range"])
def _handle_diagnostics(self, params):
if not params: return
uri = params.get("uri", "")
diagnostics = params.get("diagnostics", [])
errors = []
warnings = []
hints = []
for diag in diagnostics:
severity = diag.get("severity", 1)
message = diag.get("message", "")
range = diag.get("range", {})
diag_info = {
"message": message,
"range": range
}
if severity == 1:
errors.append(diag_info)
elif severity == 2:
warnings.append(diag_info)
elif severity == 3:
hints.append(diag_info)
self.response_cache.lsp_diagnostics = {
"uri": uri,
"errors": errors,
"warnings": warnings,
"hints": hints
}
logger.debug(f"LSP Diagnostics for {uri}: {len(errors)} errors, {len(warnings)} warnings, {len(hints)} hints")

View File

@@ -0,0 +1,51 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .default import DefaultHandler
class JavaHandler(DefaultHandler):
"""Java-specific: overrides definition, handles classFileContents."""
def handle(self, method: str, response, controller):
match method:
case "textDocument/definition":
self._handle_definition(response, controller)
case "java/classFileContents":
self._handle_class_file_contents(response)
case _:
super().handle(method, response, controller)
def _handle_definition(self, response, controller):
if not response: return
uri = response[0]["uri"]
if "jdt://" in uri:
controller._lsp_java_class_file_contents(uri)
return
self.context._prompt_goto_request(uri, response[0]["range"])
def _handle_class_file_contents(self, text: str):
event = Event_Factory.create_event("get_active_view")
self.context.emit_to("source_views", event)
view = event.response
file = view.command.exec("new_file")
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark(buffer.get_insert())
lm = GtkSource.LanguageManager.get_default()
language = lm.get_language("java")
file.ftype = "java"
buffer.set_language(language)
buffer.insert(itr, text, -1)

View File

@@ -0,0 +1,12 @@
# Python imports
# Lib imports
# Application imports
from .default import DefaultHandler
class PythonHandler(DefaultHandler):
"""Uses default handling, can override if Python needs special logic."""
...

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
# Application imports
from ..mixins.lsp_server_events_mixin import LSPServerEventsMixin
from .base import BaseHandler
from .default import DefaultHandler
from .python import PythonHandler
from .java import JavaHandler
class HandlerRegistry(LSPServerEventsMixin):
def __init__(self):
self._instances: dict = {}
self._lang_handlers: dict = {
"default": DefaultHandler,
"python": PythonHandler,
"java": JavaHandler,
}
def _get_instance(self, handler_cls: type[BaseHandler]) -> BaseHandler:
if handler_cls in self._instances: return self._instances[handler_cls]
self._instances[handler_cls] = handler_cls()
return self._instances[handler_cls]
def register_handler(self, lang_id: str, handler_cls: type[BaseHandler]):
self._lang_handlers[lang_id] = handler_cls
def get_handler(self, lang_id: str = "", method: str = ""):
handler_cls = self._lang_handlers.get(
lang_id, self._lang_handlers.get("default", DefaultHandler)
)
if not handler_cls: return None
return self._get_instance(handler_cls)
def close_handler(self, lang_id: str):
if not lang_id in self._lang_handlers: return
handler_cls = self._lang_handlers[lang_id]
self._instances.pop(handler_cls, None)

View File

@@ -0,0 +1,57 @@
# Python imports
from concurrent.futures import ThreadPoolExecutor
# Lib imports
# Application imports
from .mixins.lsp_client_events_mixin import LSPClientEventsMixin
from .controllers.lsp_controller import LSPController
class LSPClientController(LSPClientEventsMixin):
def __init__(self):
super(LSPClientController, self).__init__()
self._cache_refresh_timeout_id: int = None
self.executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers = 1)
self.active_language_id: str = ""
self.clients: dict = {}
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {}
) -> LSPController:
if lang_id in self.clients: return None
address = "127.0.0.1"
port = 9999
uri = f"ws://{address}:{port}/{lang_id}"
client = LSPController()
client.set_language(lang_id)
client.set_socket(uri)
client.start_client()
if not client.ws_client.wait_for_connection(timeout = 5.0):
logger.error(f"Failed to connect to LSP server for {lang_id}")
return None
self.clients[lang_id] = client
return client
def close_client(self, lang_id: str) -> bool:
if lang_id not in self.clients: return False
controller = self.clients.pop(lang_id)
controller.stop_client()
return True
def get_active_client(self) -> LSPController:
return self.clients[self.active_language_id]

View File

@@ -0,0 +1,100 @@
# Python imports
# Lib imports
# Application imports
from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, LSPResponseRequest, LSPResponseNotification
from .provider import Provider
from .provider_response_cache import ProviderResponseCache
from .lsp_manager_ui import LSPManagerUI
from .lsp_client_controller import LSPClientController
from .handlers.registry import HandlerRegistry
class LSPController:
def __init__(self):
super(LSPController, self).__init__()
self._init()
self._load_widgets()
self._do_bind_mapping()
def _init(self):
self.provider: Provider = Provider()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
self.lsp_client_controller: LSPClientController = LSPClientController()
self.handler_registry: HandlerRegistry = HandlerRegistry()
def _load_widgets(self):
self.lsp_manager_ui: LSPManagerUI = LSPManagerUI()
self.lsp_manager_ui.create_client = self.create_client
self.lsp_manager_ui.close_client = self.close_client
def _do_bind_mapping(self):
self.response_cache.process_file_load = self.lsp_client_controller.process_file_load
self.response_cache.process_file_close = self.lsp_client_controller.process_file_close
self.response_cache.process_file_save = self.lsp_client_controller.process_file_save
self.response_cache.process_file_change = self.lsp_client_controller.process_file_change
self.provider.response_cache = self.response_cache
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {}
) -> bool:
client = self.lsp_client_controller.create_client(
lang_id, workspace_uri, init_opts
)
handler = self.handler_registry.get_handler(lang_id)
self.lsp_client_controller.active_language_id = lang_id
if not client or not handler:
logger.error(f"LSP Controller: Either 'client' or 'handler' didn't get created...'")
self.close_client(lang_id)
return False
handler.set_context(self.handler_registry)
handler.set_response_cache(self.response_cache)
client.handle_lsp_response = self.server_response
client.send_initialize_message(init_opts, "", f"file://{workspace_uri}")
return True
def close_client(self, lang_id: str) -> bool:
self.lsp_client_controller.close_client(lang_id)
self.handler_registry.close_handler(lang_id)
return True
def server_response(self, lsp_response: LSPResponseTypes):
logger.debug(f"LSP Response: { lsp_response }")
if isinstance(lsp_response, LSPResponseRequest):
if not self.lsp_client_controller.active_language_id in self.lsp_client_controller.clients:
logger.debug(f"No LSP client for '{self.lsp_client_controller.active_language_id}', skipping 'server_response'")
return
controller = self.lsp_client_controller.get_active_client()
event = controller.get_event_by_id(lsp_response.id)
handler = self.handler_registry.get_handler(
self.lsp_client_controller.active_language_id, event
)
if not handler: return
handler.handle(event, lsp_response.result, controller)
elif isinstance(lsp_response, LSPResponseNotification):
handler = self.handler_registry.get_handler("default", lsp_response.method)
if not handler: return
# TODO: Need to make default singleton so as to not need to set these here
handler.set_context(self.handler_registry)
handler.set_response_cache(self.response_cache)
handler.handle(lsp_response.method, lsp_response.params, None)

View File

@@ -12,20 +12,18 @@ from gi.repository import GLib
from gi.repository import GtkSource from gi.repository import GtkSource
# Application imports # Application imports
from .provider import Provider
class LSPManager(Gtk.Dialog): class LSPManagerUI(Gtk.Dialog):
def __init__(self): def __init__(self):
super(LSPManager, self).__init__() super(LSPManagerUI, self).__init__()
self._SCRIPT_PTH: str = path.dirname( path.realpath(__file__) ) self._SCRIPT_PTH: str = path.dirname( path.realpath(__file__) )
self._USER_HOME: str = path.expanduser('~') self._USER_HOME: str = path.expanduser('~')
self._LSP_SERVERS_CONFIG: str = "" self._LSP_SERVERS_CONFIG: str = ""
self.servers_config: dict = {} self.servers_config: dict = {}
self.provider: Provider = Provider()
self.parent = None self.parent = None
self.source_view = None self.source_view = None
@@ -69,8 +67,8 @@ class LSPManager(Gtk.Dialog):
self.path_bttn.connect("file-set", self._file_set) self.path_bttn.connect("file-set", self._file_set)
self.path_bttn.set_halign(Gtk.Align.FILL) self.path_bttn.set_halign(Gtk.Align.FILL)
self.hide_bttn.connect("clicked", lambda widget: self.hide()) self.hide_bttn.connect("clicked", lambda widget: self.hide())
create_client_bttn.connect("clicked", self.create_client, close_client_bttn) create_client_bttn.connect("clicked", self._create_client, close_client_bttn)
close_client_bttn.connect("clicked", self.close_client, create_client_bttn) close_client_bttn.connect("clicked", self._close_client, create_client_bttn)
self.main_box.set_column_spacing(15) self.main_box.set_column_spacing(15)
self.main_box.set_row_spacing(15) self.main_box.set_row_spacing(15)
@@ -110,8 +108,7 @@ class LSPManager(Gtk.Dialog):
widget.move(x, y) widget.move(x, y)
def _path_changed(self, widget, buttons_widget): def _path_changed(self, widget, buttons_widget):
fpath = widget.get_text() if not widget.get_text():
if not fpath:
buttons_widget.hide() buttons_widget.hide()
return return
@@ -145,10 +142,16 @@ class LSPManager(Gtk.Dialog):
scrolled_win.show_all() scrolled_win.show_all()
def load_lsp_servers_config(self): def load_lsp_servers_config(self):
with open(f"{self._SCRIPT_PTH}/configs/lsp-servers-config.json") as file: try:
self._LSP_SERVERS_CONFIG = file.read() with open(f"{self._SCRIPT_PTH}/configs/lsp-servers-config.json") as file:
self._LSP_SERVERS_CONFIG = file.read()
except FileNotFoundError:
logger.error(f"Config file not found: {self._SCRIPT_PTH}/configs/lsp-servers-config.json")
def load_lsp_servers_config_placeholders(self): def load_lsp_servers_config_placeholders(self):
if not self._LSP_SERVERS_CONFIG: return
if not self.source_view: return
data = self._LSP_SERVERS_CONFIG \ data = self._LSP_SERVERS_CONFIG \
.replace("{user.home}", self._USER_HOME) \ .replace("{user.home}", self._USER_HOME) \
.replace("{workspace.folder}", self.path_entry.get_text()) .replace("{workspace.folder}", self.path_entry.get_text())
@@ -162,24 +165,36 @@ class LSPManager(Gtk.Dialog):
buffer.delete(start_itr, end_itr) buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, data, -1) buffer.insert(start_itr, data, -1)
self.set_language_combo_box( self.servers_config.keys() ) self.set_language_combo_box( list(self.servers_config.keys()) )
def set_language_combo_box(self, lang_ids: list[str]): def set_language_combo_box(self, lang_ids: list[str]):
self.combo_box.remove_all()
for lang_id in lang_ids: for lang_id in lang_ids:
self.combo_box.append_text(lang_id) self.combo_box.append_text(lang_id)
def create_client(self, widget, sibling): def _create_client(self, widget, sibling):
if not self.source_view: return
buffer = self.source_view.get_buffer() buffer = self.source_view.get_buffer()
lang_id = self.combo_box.get_active_text() lang_id = self.combo_box.get_active_text()
if not lang_id: return if not lang_id: return
if not lang_id in self.servers_config: return if not lang_id in self.servers_config: return
self.servers_config = json.loads( buffer.get_text( *buffer.get_bounds() ) ) try:
init_opts = self.servers_config[lang_id]["initialization-options"] self.servers_config = json.loads(
workspace_dir = self.path_entry.get_text() buffer.get_text( *buffer.get_bounds() )
)
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON: {e}")
return
result = self.provider.response_cache.create_client( init_opts = self.servers_config[lang_id]["initialization-options"]
workspace_dir = self.path_entry.get_text()
result = None
result = self.create_client(
lang_id, workspace_dir, init_opts lang_id, workspace_dir, init_opts
) )
@@ -188,11 +203,11 @@ class LSPManager(Gtk.Dialog):
widget.hide() widget.hide()
sibling.show() sibling.show()
def close_client(self, widget, sibling): def _close_client(self, widget, sibling):
lang_id = self.combo_box.get_active_text() lang_id = self.combo_box.get_active_text()
if not lang_id: return if not lang_id: return
result = self.provider.response_cache.close_client(lang_id) result = self.close_client(lang_id)
if not result: return if not result: return
widget.hide() widget.hide()

View File

@@ -23,7 +23,7 @@ class LSPClientEventsMixin:
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds()) text = buffer.get_text(*buffer.get_bounds())
self._last_active_language_id = lang_id self.active_language_id = lang_id
controller._lsp_did_open({ controller._lsp_did_open({
"uri": uri, "uri": uri,
@@ -54,7 +54,7 @@ class LSPClientEventsMixin:
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds()) text = buffer.get_text(*buffer.get_bounds())
self._last_active_language_id = lang_id self.active_language_id = lang_id
controller._lsp_did_save({"uri": uri, "text": text}) controller._lsp_did_save({"uri": uri, "text": text})
@@ -71,7 +71,7 @@ class LSPClientEventsMixin:
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds()) text = buffer.get_text(*buffer.get_bounds())
self._last_active_language_id = lang_id self.active_language_id = lang_id
controller._lsp_did_change({ controller._lsp_did_change({
"uri": uri, "uri": uri,
@@ -97,7 +97,7 @@ class LSPClientEventsMixin:
controller = self.clients[lang_id] controller = self.clients[lang_id]
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
self._last_active_language_id = lang_id self.active_language_id = lang_id
controller._lsp_definition({ controller._lsp_definition({
"uri": uri, "uri": uri,
@@ -116,7 +116,7 @@ class LSPClientEventsMixin:
controller = self.clients[lang_id] controller = self.clients[lang_id]
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
self._last_active_language_id = lang_id self.active_language_id = lang_id
controller._lsp_completion({ controller._lsp_completion({
"uri": uri, "uri": uri,

View File

@@ -29,22 +29,25 @@ class LSPServerEventsMixin:
self.matchers.clear() self.matchers.clear()
for item in items: for item in items:
label = item.get("label", "") label = item.get("label")
if not label: continue if not label: return None
text = item.get("insertText") text = (
if not text and "textEdit" in item: item.get("insertText")
text = item["textEdit"].get("newText", "") or item.get("textEdit", {}).get("newText")
or item.get("textEditText", "")
or label
)
info = "" detail = item.get("detail")
if "detail" in item: doc = item.get("documentation")
info = item["detail"]
elif "documentation" in item: if detail:
doc = item["documentation"] info = detail
if isinstance(doc, dict): elif isinstance(doc, dict):
info = doc.get("value", "") info = doc.get("value", "")
else: else:
info = str(doc) info = str(doc) if doc else ""
self.matchers[label] = { self.matchers[label] = {
"label": label, "label": label,

View File

@@ -1,6 +1,9 @@
# Python imports # Python imports
# Lib imports # Lib imports
import gi
from gi.repository import GLib
# Application imports # Application imports
from libs.event_factory import Event_Factory, Code_Event_Types from libs.event_factory import Event_Factory, Code_Event_Types
@@ -8,11 +11,11 @@ from libs.dto.states import SourceViewStates
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
from .lsp_manager import LSPManager from .lsp_controller import LSPController
lsp_manager = LSPManager() lsp_controller = LSPController()
@@ -27,7 +30,7 @@ class Plugin(PluginCode):
def load(self): def load(self):
window = self.request_ui_element("main-window") window = self.request_ui_element("main-window")
lsp_manager.map_parent_resize_event(window) lsp_controller.lsp_manager_ui.map_parent_resize_event(window)
event = Event_Factory.create_event("register_command", event = Event_Factory.create_event("register_command",
command_name = "LSP Manager", command_name = "LSP Manager",
@@ -37,10 +40,10 @@ class Plugin(PluginCode):
) )
self.emit_to("source_views", event) self.emit_to("source_views", event)
event = Event_Factory.create_event( event = Event_Factory.create_event(
"register_provider", "register_provider",
provider_name = "LSP Completer", provider_name = "LSP Completer",
provider = lsp_manager.provider, provider = lsp_controller.provider,
language_ids = [] language_ids = []
) )
self.emit_to("completion", event) self.emit_to("completion", event)
@@ -52,12 +55,14 @@ class Plugin(PluginCode):
self.emit_to("source_views", event) self.emit_to("source_views", event)
source_view = event.response source_view = event.response
lsp_manager.load_lsp_servers_config() lsp_controller.lsp_manager_ui.load_lsp_servers_config()
lsp_manager.set_source_view(source_view) lsp_controller.lsp_manager_ui.set_source_view(source_view)
lsp_manager.load_lsp_servers_config_placeholders() lsp_controller.lsp_manager_ui.load_lsp_servers_config_placeholders()
lsp_manager.provider.response_cache.emit = self.emit
lsp_manager.provider.response_cache.emit_to = self.emit_to lsp_controller.handler_registry.emit = self.emit
lsp_manager.provider.response_cache._prompt_completion_request = self._prompt_completion_request lsp_controller.handler_registry.emit_to = self.emit_to
lsp_controller.handler_registry._prompt_goto_request = self._prompt_goto_request
lsp_controller.handler_registry._prompt_completion_request = self._prompt_completion_request
def run(self): def run(self):
... ...
@@ -65,6 +70,24 @@ class Plugin(PluginCode):
def generate_plugin_element(self): def generate_plugin_element(self):
... ...
def _prompt_goto_request(self, uri: str, pointer_pos: dict):
event = Event_Factory.create_event(
"get_active_view",
)
self.emit_to("source_views", event)
view = event.response
view._on_uri_data_received( [uri] )
buffer = view.get_buffer()
def move_cursor(buffer, pointer_pos):
itr = buffer.get_iter_at_line( pointer_pos["end"]["line"] )
itr.forward_chars( pointer_pos["end"]["character"] )
buffer.place_cursor(itr)
view.scroll_to_iter(itr, 0.2, False, 0, 0)
GLib.idle_add( move_cursor, buffer, pointer_pos )
def _prompt_completion_request(self): def _prompt_completion_request(self):
event = Event_Factory.create_event( event = Event_Factory.create_event(
"get_active_view", "get_active_view",
@@ -75,7 +98,7 @@ class Plugin(PluginCode):
event = Event_Factory.create_event( event = Event_Factory.create_event(
"request_completion", "request_completion",
view = view, view = view,
provider = lsp_manager.provider provider = lsp_controller.provider
) )
self.emit_to("completion", event) self.emit_to("completion", event)
@@ -98,7 +121,7 @@ class Handler:
column = iter.get_line_offset() column = iter.get_line_offset()
if char_str == "g": if char_str == "g":
lsp_manager.provider.response_cache.process_goto_definition( lsp_controller.lsp_client_controller.process_goto_definition(
file.ftype, file.fpath, line, column file.ftype, file.fpath, line, column
) )
@@ -107,4 +130,4 @@ class Handler:
if char_str == "i": if char_str == "i":
return return
lsp_manager.hide() if lsp_manager.is_visible() else lsp_manager.show() lsp_controller.lsp_manager_ui.hide() if lsp_controller.lsp_manager_ui.is_visible() else lsp_controller.lsp_manager_ui.show()

View File

@@ -22,7 +22,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
def __init__(self): def __init__(self):
super(Provider, self).__init__() super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache() self.response_cache: ProviderResponseCache = None
def pre_populate(self, context): def pre_populate(self, context):
@@ -32,13 +32,19 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
return "LSP Code Completion" return "LSP Code Completion"
def do_match(self, context): def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
# completion = context.get_property("completion")
# if not completion.get_view().has_focus(): return
iter = self.response_cache.get_iter_correctly(context) iter = self.response_cache.get_iter_correctly(context)
iter.backward_char() iter.backward_char()
ch = iter.get_char() ch = iter.get_char()
# NOTE: Look to re-add or apply supprting logic to use spaces # NOTE: Look to re-add or apply supporting logic to use spaces
# As is it slows down the editor in certain contexts... # As is it slows down the editor in certain contexts...
if not (ch in ('_', '.', ' ') or ch.isalnum()): # if not (ch in ('_', '.', ' ') or ch.isalnum()):
if not (ch in ('_', '.') or ch.isalnum()):
return False return False
buffer = iter.get_buffer() buffer = iter.get_buffer()

View File

@@ -1,7 +1,5 @@
# Python imports # Python imports
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import asyncio
from asyncio import Queue
# Lib imports # Lib imports
import gi import gi
@@ -14,113 +12,17 @@ from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, LSPResponseR
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
from .controllers.lsp_controller import LSPController
from .mixins.lsp_client_events_mixin import LSPClientEventsMixin
from .mixins.lsp_server_events_mixin import LSPServerEventsMixin
class ProviderResponseCache(ProviderResponseCacheBase):
class ProviderResponseCache(LSPClientEventsMixin, LSPServerEventsMixin, ProviderResponseCacheBase):
def __init__(self): def __init__(self):
super(ProviderResponseCache, self).__init__() super(ProviderResponseCache, self).__init__()
self.executor = ThreadPoolExecutor(max_workers = 1)
self.matchers: dict = {} self.matchers: dict = {}
self.clients: dict = {}
self._cache_refresh_timeout_id: int = None
self._last_active_language_id: str = None
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {
}) -> bool:
if lang_id in self.clients: return False
address = "127.0.0.1"
port = 9999
uri = f"ws://{address}:{port}/{lang_id}"
controller = LSPController()
controller.handle_lsp_response = self.server_response
controller.set_language(lang_id)
controller.set_socket(uri)
controller.start_client()
if not controller.ws_client.wait_for_connection(timeout = 5.0):
logger.error(f"Failed to connect to LSP server for {lang_id}")
return False
self.clients[lang_id] = controller
controller.send_initialize_message(init_opts, "", f"file://{workspace_uri}")
return True
def close_client(self, lang_id: str) -> bool:
if lang_id not in self.clients: return False
controller = self.clients.pop(lang_id)
controller.stop_client()
return True
# TODO: Need to map 'lang_id' to a given language response class and
# pass the controller to a 'server_response' method there.
# It would allow clean separation of each language's idiosyncracies
def server_response(self, lsp_response: LSPResponseTypes):
logger.debug(f"LSP Response: { lsp_response }")
if isinstance(lsp_response, LSPResponseRequest):
if not self._last_active_language_id in self.clients:
logger.debug(f"No LSP client for '{self._last_active_language_id}', skipping 'server_response'")
return
controller = self.clients[self._last_active_language_id]
event = controller.get_event_by_id(lsp_response.id)
match event:
case "textDocument/completion":
self._handle_completion_response(lsp_response.result)
case "textDocument/definition":
result = lsp_response.result
if not result: return
uri = result[0]["uri"]
if "jdt://" in uri:
controller._lsp_java_class_file_contents(uri)
return
self._handle_definition_response(uri, result[0]["range"])
case "java/classFileContents":
self._handle_java_class_file_contents(lsp_response.result)
case _:
...
elif isinstance(lsp_response, LSPResponseNotification):
match lsp_response.method:
case "textDocument/publishDiagnostics":
...
case _:
...
def filter(self, word: str) -> list[dict]: def filter(self, word: str) -> list[dict]:
return [] return []
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]: def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
response = [] return list( self.matchers.values() )
iter = self.get_iter_correctly(context)
iter.backward_char()
char_str = iter.get_char()
if char_str == "." or char_str == " ":
for label, item in self.matchers.items():
response.append(item)
return response
word = self.get_word(context).rstrip()
for label, item in self.matchers.items():
if label.startswith(word):
response.append(item)
return response

View File

@@ -0,0 +1,8 @@
#!/bin/bash
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
CONTAINER="newton-lsp"

View File

@@ -0,0 +1,38 @@
#!/bin/bash
. CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
cd "${SCRIPTPATH}"
echo "Working Dir: " $(pwd)
ID=$(podman ps --filter "ancestor=localhost/${CONTAINER}:latest" --format "{{.ID}}")
if [ "${ID}" != "" ]; then
echo "Is up..."
exit 1
fi
CODE_HOST="${HOME}/Coding"
CODE_CONTAINER="${HOME}/Coding"
CONFIG_HOST="${HOME}/.config/lsps"
CONFIG_CONTAINER="${HOME}/.config/lsps"
# podman run -d -m 4G \
podman run -m 4G \
-p 9999:9999 \
-e HOME="${HOME}" \
-e MAVEN_OPTS="-Duser.home=${HOME}" \
-e JAVA_TOOL_OPTIONS="-Duser.home=${HOME}" \
-e JDTLS_CONFIG_PATH="${CONFIG_CONTAINER}/jdtls" \
-e JDTLS_DATA_PATH="${JDTLS_CONFIG_PATH}/data" \
-v "${CODE_HOST}":"${CODE_CONTAINER}" \
-v "${CONFIG_HOST}":"${CONFIG_CONTAINER}" \
"${CONTAINER}:latest"
}
main $@;

View File

@@ -0,0 +1,23 @@
#!/bin/bash
. CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
cd "${SCRIPTPATH}"
echo "Working Dir: " $(pwd)
ID=$(podman ps --filter "ancestor=localhost/${CONTAINER}:latest" --format "{{.ID}}")
if [ "${ID}" == "" ]; then
echo "Is not up..."
exit 1
fi
podman container stop "${ID}"
}
main $@;

View File

@@ -9,6 +9,7 @@ from gi.repository import GtkSource
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import Gio from gi.repository import Gio
from gi.repository import GLib
# Application imports # Application imports
from ..command_helpers import update_info_bar_if_focused from ..command_helpers import update_info_bar_if_focused
@@ -35,6 +36,10 @@ def execute(
update_info_bar_if_focused(view.command, view) update_info_bar_if_focused(view.command, view)
view.emit("focus-in-event", Gdk.Event()) view.emit("focus-in-event", Gdk.Event())
buffer = view.get_buffer() def scroll_to_insert_itr(view):
itr = buffer.get_iter_at_mark( buffer.get_insert() ) buffer = view.get_buffer()
view.scroll_to_iter(itr, 0.2, False, 0, 0) itr = buffer.get_iter_at_mark( buffer.get_insert() )
view.scroll_to_iter(itr, 0.2, False, 0, 0)
GLib.idle_add(scroll_to_insert_itr, view)