Compare commits

10 Commits

Author SHA1 Message Date
77a3b71d31 feat: Complete plugin lifecycle management with lazy loading and runtime reload
Major changes:
- Add unload() method to all plugins for proper cleanup (unregister commands/providers/LSP clients, destroy widgets, clear state)
- Implement lazy widget loading via "show" signal across all containers
- Add autoload: false manifest option for manual/conditional plugin loading
- Add Plugins UI with runtime load/unload toggle via Ctrl+Shift+p
- Implement controller unregistration system with proper signal disconnection
- Add new events: UnregisterCommandEvent, GetFilesEvent, GetSourceViewsEvent, TogglePluginsUiEvent
- Fix signal leaks by tracking and disconnecting handlers in widgets (search/replace, LSP manager, tabs, telescope, markdown preview)
- Add Save/Save As to tabs context menu
- Improve search/replace behavior (selection handling, focus management)
- Add telescope file initialization from existing loaded files
- Refactor plugin reload watcher to dynamically add/remove plugins on filesystem changes
- Add new plugins: file_history, extend_source_view_menu, godot_lsp_client
- Fix bug in prettify_json (undefined variable reference)
2026-03-21 13:22:20 -05:00
21dd86ad3d Add dynamic EventNamespace for event type access and refactor LSP manager initialization
- Create EventNamespace class to enable Code_Event_Types.RegisterLspClientEvent access
- Move set_event_hub call from plugin to lsp_manager._do_bind_mapping
- Update event type references to use Code_Event_Types namespace
2026-03-15 23:41:09 -05:00
fea303c898 Remove custom LSP manager plugins and add new language server clients
- Delete old lsp_manager plugin (custom websocket-based LSP client implementation)
- Delete java_lsp_client plugin
- Delete python_lsp_client plugin
- Remove unused LSP DTO files in src/libs/dto/code/lsp/
- Add new language_server_clients plugin directory
- Improve event_factory with register_events method
- Add PYTHONDONTWRITEBYTECODE to user config
- Update events init.py docstring
2026-03-15 20:47:40 -05:00
e8653cd116 refactor: remove LSPServerEventsMixin and clean up websocket tests
- Delete unused websocket library test files
- Remove LSPServerEventsMixin and inline its methods into response handlers
- Clean up unused imports (ThreadPoolExecutor, ABC, LSP message structs)
2026-03-15 03:36:01 -05:00
5e28fb1e5c refactor(lsp-manager): replace handler architecture with response registry and modular providers
* Remove legacy handlers system (BaseHandler, DefaultHandler, JavaHandler, PythonHandler, HandlerRegistry)
* Introduce response_handlers module with ResponseRegistry for LSP response routing
* Replace HandlerRegistry usage in LSPManager with ResponseRegistry
* Convert LSPManagerUI client lifecycle to GObject signals
* Remove GLib.idle_add usage in LSP client event dispatch
* Move completion request logic into LSPServerEventsMixin
* Replace provider.py and provider_response_cache.py with modular provider/ package
* Simplify plugin wiring via response_registry.set_event_hub()

This refactor decouples response handling, simplifies event flow, and prepares
the LSP manager for easier language-specific extensions.
2026-03-15 01:52:15 -05:00
609eaa8246 refactor(lsp): replace controller layer with client module and LSPManager orchestration
* Rename legacy controller subsystem (LSPController, websocket controller, controller events, and base classes)
    into clarified client module for LSP communication
* Structure around LSPManager and LSPManagerClient to handle orchestration and client lifecycle
* Update plugin integration to use LSPManager instead of LSPController
* Simplify architecture by reducing controller indirection
2026-03-12 00:07:59 -05:00
71bab687d7 refactor(lsp): replace LSPManager with controller-based architecture
- Remove legacy LSPManager dialog implementation
- Introduce LSPController as the central LSP entry point
- Route UI interactions through lsp_controller.lsp_manager_ui
- Move client lifecycle handling out of ProviderResponseCache
- Simplify completion cache and matcher filtering
- Improve LSP completion item parsing with safer fallbacks
- Modernize typing (Python 3.10 union syntax)
- Remove unused imports and dead code
- Use GLib.idle_add for safe UI scrolling operations
- Minor comment and spelling fixes
2026-03-11 23:17:57 -05:00
060f68237b feat(lsp): support Java class file contents and improve definition navigation handling
- Add `_lsp_java_class_file_contents` request to fetch contents of compiled Java classes via LSP (`java/classFileContents`).
- Handle `java/classFileContents` responses by opening a new buffer with Java syntax highlighting and inserting the returned source.
- Update definition handling to pass URI and range, enabling precise cursor placement after navigation.
- Detect `jdt://` URIs in `textDocument/definition` responses and request class file contents instead of direct navigation.
- Move goto navigation logic into `LSPServerEventsMixin`, using event system to access the active view and position the cursor.
- Expose `emit` and `emit_to` to the response cache for event dispatching.
- Restrict completion activation to `USER_REQUESTED`.
- Add TODO note about mapping language IDs to dedicated response handlers.
2026-03-08 17:53:12 -05:00
ee5f66fbbb Fix on load scroll pointer to view 2026-03-08 15:09:19 -05:00
7ee484f0c0 Remove temp cut buffer commands and cleanup related SourceView code
- Moved cut_to_temp_buffer and paste_temp_buffer commands to plugin
- Remove temp buffer state and timeout logic from SourceView
- Remove associated keybindings (Ctrl+K / Ctrl+U)
- Ignore "buffer" pseudo-path when filtering loaded files
- Ensure view receives focus after DnD file load
2026-03-08 14:51:11 -05:00
180 changed files with 3063 additions and 2859 deletions

22
TODO.md Normal file
View File

@@ -0,0 +1,22 @@
___
### Add
1. Add Godot LSP Client
1. Add Collapsable code blocks
1. Add Plugin to <Shift\><Ctrl\>| and <Ctrl\>| to split views up, down, left, right
1. Add <Ctrl\>i to **lsp_manager** to list who implements xyz
___
### Change
1. Make **telescope** plugin a generic base to allow query mode additions through plugins
1. Make **lsp_manager** hard coded values configurable, plus add fields to UI
___
### Fix
- Fix **telescope** search selection when items hidden such that
hidden items don't get selected on up/down key
- Fix to make acive tab on **tabs_bar** scroll to center
- Fix **file_state_watcher** to prompt refrsh if external changes applied
- Fix on lsp client unload to close files lsp side and unload server endpoint
- Fix multi-select <Shift\><Ctrl\> left/right block select movement de-sync
from leader when '_' in word
___

View File

@@ -39,6 +39,24 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "autopairs",
command = Handler,
binding_mode = "held",
binding = [
"'", "`", "[", "]",
'<Shift>"',
'<Shift>(',
'<Shift>)',
'<Shift>{',
'<Shift>}'
]
)
self.emit_to("source_views", event)
autopairs = None
def run(self):
...

View File

@@ -27,7 +27,19 @@ class Plugin(PluginCode):
colorize.handle_colorize(event.buffer)
def load(self):
event = Event_Factory.create_event("register_command",
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
event = Event_Factory.create_event("get_source_views")
self.emit_to("source_views", event)
for view in event.response:
buffer = view.get_buffer()
colorize.clear_color_tags(buffer)
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "tggle_colorize",
command = Handler,
binding_mode = "released",
@@ -36,6 +48,7 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def run(self):
...

View File

@@ -36,6 +36,16 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "keyboard_tggl_comment",
command = Handler,
binding_mode = "released",
binding = "<Control>slash"
)
self.emit_to("source_views", event)
def run(self):
...

View File

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

View File

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

View File

@@ -0,0 +1,97 @@
# Python imports
# Lib imports
# Application imports
class Autopairs:
def __init__(self):
...
def handle_word_wrap(self, buffer, char_str: str):
wrap_block = self.get_wrap_block(char_str)
if not wrap_block: return
selection = buffer.get_selection_bounds()
if not selection:
self.insert_pair(buffer, char_str, wrap_block)
return True
self.wrap_selection(buffer, char_str, wrap_block, selection)
return True
def insert_pair(
self, buffer, char_str: str, wrap_block: tuple
):
buffer.begin_user_action()
left_block, right_block = wrap_block
insert_mark = buffer.get_insert()
insert_itr = buffer.get_iter_at_mark(insert_mark)
buffer.insert(insert_itr, f"{left_block}{right_block}")
insert_itr = buffer.get_iter_at_mark( insert_mark )
insert_itr.backward_char()
buffer.place_cursor(insert_itr)
buffer.end_user_action()
def wrap_selection(
self, buffer, char_str: str, wrap_block: tuple, selection
):
left_block, \
right_block = wrap_block
start_itr, \
end_itr = selection
data = buffer.get_text(
start_itr, end_itr, include_hidden_chars = False
)
start_mark = buffer.create_mark("startclose", start_itr, False)
end_mark = buffer.create_mark("endclose", end_itr, True)
buffer.begin_user_action()
buffer.insert(start_itr, left_block)
end_itr = buffer.get_iter_at_mark(end_mark)
buffer.insert(end_itr, right_block)
start = buffer.get_iter_at_mark(start_mark)
end = buffer.get_iter_at_mark(end_mark)
buffer.select_range(start, end)
buffer.delete_mark_by_name("startclose")
buffer.delete_mark_by_name("endclose")
buffer.end_user_action()
def get_wrap_block(self, char_str) -> tuple:
left_block = ""
right_block = ""
match char_str:
case "(" | ")":
left_block = "("
right_block = ")"
case "[" | "]":
left_block = "["
right_block = "]"
case "{" | "}":
left_block = "{"
right_block = "}"
case '"':
left_block = '"'
right_block = '"'
case "'":
left_block = "'"
right_block = "'"
case "`":
left_block = "`"
right_block = "`"
case _:
return ()
return left_block, right_block

View File

@@ -0,0 +1,7 @@
{
"name": "File History",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,65 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
history: list = []
history_size: int = 30
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.RemovedFileEvent):
if event.file.ftype == "buffer": return
if len(history) == history_size:
history.pop(0)
history.append(event.file)
def load(self):
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "file_history_pop",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>t"
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: File History")
if len(history) == 0: return
view._on_uri_data_received(
[
history.pop().replace("file://", "")
]
)

View File

@@ -0,0 +1,44 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler:
@staticmethod
def execute(
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Cut to Temp Buffer")
clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark(buffer.get_insert())
start_itr = itr.copy()
start_itr.set_line_offset(0)
end_itr = start_itr.copy()
if not end_itr.forward_line():
end_itr = buffer.get_end_iter()
if not hasattr(view, "_cut_buffer"):
view._cut_buffer = ""
line_str = buffer.get_text(start_itr, end_itr, True)
view._cut_buffer += line_str
buffer.delete(start_itr, end_itr)
set_temp_cut_buffer_delayed(view)

View File

@@ -0,0 +1,24 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
def clear_temp_cut_buffer_delayed(view: any):
if not hasattr(view, "_cut_temp_timeout_id"): return
if not view._cut_temp_timeout_id: return
GLib.source_remove(view._cut_temp_timeout_id)
def set_temp_cut_buffer_delayed(view: any):
def clear_temp_buffer(view: any):
view._cut_buffer = ""
view._cut_temp_timeout_id = None
return False
view._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer, view)

View File

@@ -0,0 +1,7 @@
{
"name": "Nanoesq Temp Buffer",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,35 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler2:
@staticmethod
def execute(
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Paste Temp Buffer")
if not hasattr(view, "_cut_temp_timeout_id"): return
if not hasattr(view, "_cut_buffer"): return
if not view._cut_buffer: return
clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )
insert_itr = itr.copy()
buffer.insert(insert_itr, view._cut_buffer, -1)
set_temp_cut_buffer_delayed(view)

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .cut_to_temp_buffer import Handler
from .paste_temp_buffer import Handler2
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
self._manage_signals("register_command")
def load(self):
self._manage_signals("unregister_command")
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "cut_to_temp_buffer",
command = Handler,
binding_mode = "held",
binding = "<Control>k"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "paste_temp_buffer",
command = Handler2,
binding_mode = "held",
binding = "<Control>u"
)
self.emit_to("source_views", event)
def run(self):
...

View File

@@ -26,6 +26,16 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "toggle_source_view",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>h"
)
self.emit_to("source_views", event)
def run(self):
...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Example Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"register_provider",
provider_name = "Python Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -38,7 +38,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
iter.backward_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...
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
if not (ch in ('_', '.') or ch.isalnum()):

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Snippets Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
)
self.emit_to("completion", event)
def unload(self):
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "Words Completer"
)
self.emit_to("completion", event)
self.provider = None
del self.provider
def run(self):
...

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"name": "Extend Source View Menu",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,29 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .source_view_menu import extend_source_view_menu
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
extend_source_view_menu(event.buffer, event.menu)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -0,0 +1,68 @@
# Python imports
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
def on_case_handle(menuitem, buffer, action):
start_itr, \
end_itr = buffer.get_selection_bounds()
data = buffer.get_text(start_itr, end_itr, False)
text = data
if action == "on_all_upper":
text = data.upper()
elif action == "on_all_lower":
text = data.lower()
elif action == "on_invert":
text = data.swapcase()
elif action == "on_title":
text = data.title()
elif action == "on_title_strip":
text = data.title().replace("-", "").replace("_", "").replace(" ", "")
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
def extend_source_view_menu(buffer, menu):
if not buffer.get_selection_bounds(): return
for child in menu.get_children():
if not child.get_label() == "C_hange Case": continue
menu.remove(child)
change_case_item = Gtk.MenuItem(label = "Change Case")
case_menu = Gtk.Menu()
au_case_item = Gtk.MenuItem(label = "All Upper Case")
al_case_item = Gtk.MenuItem(label = "All Lower Case")
inver_case_item = Gtk.MenuItem(label = "Invert Case")
title_case_item = Gtk.MenuItem(label = "Title Case")
title_strip_case_item = Gtk.MenuItem(label = "Title Strip Case")
au_case_item.connect("activate", on_case_handle, buffer, "on_all_upper")
al_case_item.connect("activate", on_case_handle, buffer, "on_all_lower")
inver_case_item.connect("activate", on_case_handle, buffer, "on_invert")
title_case_item.connect("activate", on_case_handle, buffer, "on_title")
title_strip_case_item.connect("activate", on_case_handle, buffer, "on_title_strip")
case_menu.append(au_case_item)
case_menu.append(al_case_item)
case_menu.append(inver_case_item)
case_menu.append(title_case_item)
case_menu.append(title_strip_case_item)
change_case_item.set_submenu(case_menu)
menu.append(change_case_item)

View File

@@ -21,12 +21,15 @@ class Plugin(PluginCode):
event.file.check_file_on_disk()
if event.file.is_deleted():
file_is_deleted(event)
file_is_deleted(event, self.emit)
elif event.file.is_externally_modified():
file_is_externally_modified(event)
file_is_externally_modified(event, self.emit)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -10,23 +10,23 @@ from libs.event_factory import Event_Factory, Code_Event_Types
def file_is_deleted(event):
def file_is_deleted(event, emit):
event.file.was_deleted = True
event = Event_Factory.create_event(
"file_externally_deleted",
file = event.file,
buffer = event.buffer
)
self.emit(event)
emit(event)
def file_is_externally_modified(event):
def file_is_externally_modified(event, emit):
# event = Event_Factory.create_event(
# "file_externally_modified",
# file = event.file,
# buffer = event.buffer
# )
# self.emit(event)
# emit(event)
...

View File

@@ -32,5 +32,8 @@ class Plugin(PluginCode):
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -13,7 +13,7 @@ from gi.repository import Gtk
def add_prettify_json(buffer, menu):
menu.append( Gtk.SeparatorMenuItem() )
menu.append(separator)
def on_prettify_json(menuitem, buffer):
start_itr, \

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
{
"info": "https://github.com/godotengine/godot/blob/4a280218fcfdd69408cceb74577c9e69086be23a/editor/settings/editor_settings.cpp#L1132",
"command": "lsp-ws-proxy -- godot",
"alt-command": "godot",
"alt-command2": "lsp-ws-proxy --listen 4114 -- godot --headless",
"alt-command3": "godot --headless --lsp-port 7766",
"socket": "ws://127.0.0.1:9999/gdscript",
"socket-two": "ws://127.0.0.1:9999/?name=gdscript",
"initialization-options": {
"processId": ,
"clientInfo": {
"name": "Godot",
"version": "4.4"
},
"rootUri": "file://{workspace.folder}",
"capabilities": {
"workspace": {
"applyEdit": true,
"workspaceEdit": {
"documentChanges": true
}
},
"textDocument": {
"synchronization": {
"dynamicRegistration": true,
"willSave": false,
"didSave": true,
"willSaveWaitUntil": false
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
}
},
"hover": {
"dynamicRegistration": true
},
"definition": {
"dynamicRegistration": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true
}
}
},
"trace": "off"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "Godot LSP Client",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

@@ -0,0 +1,44 @@
# Python imports
from os import path
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .response_handler import GodotHandler
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
dirPth = path.dirname( path.realpath(__file__) )
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
config = f.read()
event = Event_Factory.create_event("register_lsp_client",
lang_id = "gdscript",
lang_config = config,
handler = GodotHandler
)
self.emit_to("lsp_manager", event)
def unload(self):
event = Event_Factory.create_event("unregister_lsp_client",
lang_id = "gdscript"
)
self.emit_to("lsp_manager", event)
def run(self):
...

View File

@@ -0,0 +1 @@
from .python import PythonHandler

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,214 @@
{
"info": "https://download.eclipse.org/jdtls/",
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
"link": "https://download.eclipse.org/jdtls/milestones/?d",
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
"alt-command": "lsp-ws-proxy -- jdtls",
"alt-command2": "java-language-server",
"socket": "ws://127.0.0.1:9999/java",
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
"initialization-options": {
"bundles": [
"intellicode-core.jar"
],
"workspaceFolders": [
"file://{workspace.folder}"
],
"extendedClientCapabilities": {
"classFileContentsSupport": true,
"executeClientCommandSupport": false
},
"settings": {
"java": {
"autobuild": {
"enabled": true
},
"jdt": {
"ls": {
"javac": {
"enabled": true
},
"java": {
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
},
"lombokSupport": {
"enabled": true
},
"protobufSupport":{
"enabled": true
},
"androidSupport": {
"enabled": true
}
}
},
"configuration": {
"updateBuildConfiguration": "automatic",
"maven": {
"userSettings": "{user.home}/.config/jdtls/settings.xml",
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
},
"runtimes": [
{
"name": "JavaSE-17",
"path": "/usr/lib/jvm/java-17-openjdk",
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
"default": false
},
{
"name": "JavaSE-22",
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
"default": true
}
]
},
"classPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
"lib/**/*-sources.jar"
],
"docPath": [
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
"lib/**/*-javadoc.jar"
],
"project": {
"encoding": "ignore",
"outputPath": "bin",
"referencedLibraries": [
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
"lib/**/*.jar"
],
"importOnFirstTimeStartup": "automatic",
"importHint": true,
"resourceFilters": [
"node_modules",
"\\.git"
],
"sourcePaths": [
"src",
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
]
},
"sources": {
"organizeImports": {
"starThreshold": 99,
"staticStarThreshold": 99
}
},
"imports": {
"gradle": {
"wrapper": {
"checksums": []
}
}
},
"import": {
"maven": {
"enabled": true,
"offline": {
"enabled": false
},
"disableTestClasspathFlag": false
},
"gradle": {
"enabled": false,
"wrapper": {
"enabled": true
},
"version": "",
"home": "abs(static/gradle-7.3.3)",
"java": {
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
},
"offline": {
"enabled": false
},
"arguments": [],
"jvmArguments": [],
"user": {
"home": ""
},
"annotationProcessing": {
"enabled": true
}
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**"
],
"generatesMetadataFilesAtProjectRoot": false
},
"maven": {
"downloadSources": true,
"updateSnapshots": true
},
"silentNotification": true,
"contentProvider": {
"preferred": "fernflower"
},
"signatureHelp": {
"enabled": true,
"description": {
"enabled": true
}
},
"completion": {
"enabled": true,
"engine": "ecj",
"matchCase": "firstletter",
"maxResults": 25,
"guessMethodArguments": true,
"lazyResolveTextEdit": {
"enabled": true
},
"postfix": {
"enabled": true
},
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*"
],
"importOrder": [
"#",
"java",
"javax",
"org",
"com"
]
},
"references": {
"includeAccessors": true,
"includeDecompiledSources": true
},
"codeGeneration": {
"toString": {
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
},
"insertionLocation": "afterCursor",
"useBlocks": true
},
"implementationsCodeLens": {
"enabled": true
},
"referencesCodeLens": {
"enabled": true
},
"progressReports": {
"enabled": false
},
"saveActions": {
"organizeImports": true
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "Java LSP Client",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

@@ -0,0 +1,43 @@
# Python imports
from os import path
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .response_handler import JavaHandler
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
dirPth = path.dirname( path.realpath(__file__) )
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
config = f.read()
event = Event_Factory.create_event("register_lsp_client",
lang_id = "java",
lang_config = config,
handler = JavaHandler
)
self.emit_to("lsp_manager", event)
def unload(self):
event = Event_Factory.create_event("unregister_lsp_client",
lang_id = "java"
)
self.emit_to("lsp_manager", event)
def run(self):
...

View File

@@ -0,0 +1 @@
from .java import JavaHandler

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 lsp_manager.response_handlers.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._prompt_goto_request(uri, response[0]["range"])
def _handle_class_file_contents(self, text: str):
event = Event_Factory.create_event("get_active_view")
self.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,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -0,0 +1,3 @@
"""
LSP Clients Module
"""

View File

@@ -1,36 +1,37 @@
# Python imports
import threading
from os import path
import json
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.dto.code.lsp.lsp_messages import get_message_str
from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, ClientRequest, ClientNotification
from .lsp_controller_websocket import LSPControllerWebsocket
from ..dto.code.lsp.lsp_messages import get_message_str
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification
from .lsp_client_websocket import LSPClientWebsocket
class LSPController(LSPControllerWebsocket):
class LSPClient(LSPClientWebsocket):
def __init__(self):
super(LSPController, self).__init__()
super(LSPClient, self).__init__()
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers
# initialize-params-slim.json was created off of jedi_language_server one
# self._init_params = settings_manager.get_lsp_init_data()
self._language: str = ""
self._init_params: dict = {}
self._event_history: dict[str] = {}
self._language: str = ""
self._init_params: dict = {}
self._event_history: dict[int, str] = {}
try:
from os import path
import json
_USER_HOME = path.expanduser('~')
_SCRIPT_PTH = path.dirname( path.realpath(__file__) )
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
with open(_LSP_INIT_CONFIG) as file:
data = file.read().replace("{user.home}", _USER_HOME)
self._init_params = json.loads(data)
@@ -42,7 +43,7 @@ class LSPController(LSPControllerWebsocket):
self.read_lock = threading.Lock()
self.write_lock = threading.Lock()
def set_language(self, language):
def set_language(self, language: str):
self._language = language
def set_socket(self, socket: str):
@@ -51,15 +52,15 @@ class LSPController(LSPControllerWebsocket):
def unset_socket(self):
self._socket = None
def send_notification(self, method: str, params: {} = {}):
def send_notification(self, method: str, params: dict = {}):
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._event_history[self._message_id] = method
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
return self._event_history[message_id]

View File

@@ -3,12 +3,13 @@
# Lib imports
# Application imports
from .lsp_controller_events import LSPControllerEvents
from libs.dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
from ..dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
from .lsp_client_events import LSPClientEvents
class LSPControllerBase(LSPControllerEvents):
class LSPClientBase(LSPClientEvents):
def _send_message(self, data: ClientRequest or ClientNotification):
raise NotImplementedError

View File

@@ -2,22 +2,21 @@
import os
# Lib imports
from gi.repository import GLib
# Application imports
from libs.dto.code.lsp.lsp_messages import get_message_obj
from libs.dto.code.lsp.lsp_messages import didopen_notification
from libs.dto.code.lsp.lsp_messages import didsave_notification
from libs.dto.code.lsp.lsp_messages import didclose_notification
from libs.dto.code.lsp.lsp_messages import didchange_notification
from libs.dto.code.lsp.lsp_messages import completion_request
from libs.dto.code.lsp.lsp_messages import definition_request
from libs.dto.code.lsp.lsp_messages import references_request
from libs.dto.code.lsp.lsp_messages import symbols_request
from ..dto.code.lsp.lsp_messages import get_message_obj
from ..dto.code.lsp.lsp_messages import didopen_notification
from ..dto.code.lsp.lsp_messages import didsave_notification
from ..dto.code.lsp.lsp_messages import didclose_notification
from ..dto.code.lsp.lsp_messages import didchange_notification
from ..dto.code.lsp.lsp_messages import completion_request
from ..dto.code.lsp.lsp_messages import definition_request
from ..dto.code.lsp.lsp_messages import references_request
from ..dto.code.lsp.lsp_messages import symbols_request
class LSPControllerEvents:
class LSPClientEvents:
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str):
folder_name = os.path.basename(workspace_file)
@@ -45,7 +44,7 @@ class LSPControllerEvents:
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["text"] = data["text"]
GLib.idle_add( self.send_notification, method, params )
self.send_notification( method, params )
def _lsp_did_save(self, data: dict):
method = "textDocument/didSave"
@@ -54,7 +53,7 @@ class LSPControllerEvents:
params["textDocument"]["uri"] = data["uri"]
params["text"] = data["text"]
GLib.idle_add( self.send_notification, method, params )
self.send_notification( method, params )
def _lsp_did_close(self, data: dict):
method = "textDocument/didClose"
@@ -62,7 +61,7 @@ class LSPControllerEvents:
params["textDocument"]["uri"] = data["uri"]
GLib.idle_add( self.send_notification, method, params )
self.send_notification( method, params )
def _lsp_did_change(self, data: dict):
method = "textDocument/didChange"
@@ -75,7 +74,7 @@ class LSPControllerEvents:
contentChanges = params["contentChanges"][0]
contentChanges["text"] = data["text"]
GLib.idle_add( self.send_notification, method, params )
self.send_notification( method, params )
# def _lsp_did_change(self, data: dict):
# method = "textDocument/didChange"
@@ -94,7 +93,7 @@ class LSPControllerEvents:
# end["line"] = data["line"]
# end["character"] = data["column"]
# GLib.idle_add( self.send_notification, method, params )
# self.send_notification( method, params )
def _lsp_definition(self, data: dict):
method = "textDocument/definition"
@@ -106,7 +105,7 @@ class LSPControllerEvents:
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
GLib.idle_add( self.send_request, method, params )
self.send_request( method, params )
def _lsp_completion(self, data: dict):
method = "textDocument/completion"
@@ -118,4 +117,12 @@ class LSPControllerEvents:
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
GLib.idle_add( self.send_request, method, params )
self.send_request( method, params )
def _lsp_java_class_file_contents(self, uri: str):
method = "java/classFileContents"
params = {
"uri": uri
}
self.send_request( method, params )

View File

@@ -1,23 +1,22 @@
# Python imports
import traceback
import subprocess
# Lib imports
from gi.repository import GLib
# Application imports
# 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_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from ..dto.code.lsp.lsp_messages import get_message_str, get_message_obj
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification, \
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from .lsp_controller_base import LSPControllerBase
from .lsp_client_base import LSPClientBase
from .websocket_client import WebsocketClient
class LSPControllerWebsocket(LSPControllerBase):
def _send_message(self, data: ClientRequest or ClientNotification):
class LSPClientWebsocket(LSPClientBase):
def _send_message(self, data: ClientRequest | ClientNotification):
if not data: return
message_str = get_message_str(data)
@@ -39,7 +38,7 @@ class LSPControllerWebsocket(LSPControllerBase):
if not hasattr(self, "ws_client"): return
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
message = get_message_obj(data)

View File

@@ -0,0 +1,8 @@
"""
Libs Code DTO(s) Events Package
"""
from .lsp_event import LspEvent
from .register_lsp_client_event import RegisterLspClientEvent
from .unregister_lsp_client_event import UnregisterLspClientEvent

View File

@@ -0,0 +1,13 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from libs.dto.code.events import CodeEvent
@dataclass
class LspEvent(CodeEvent):
...

View File

@@ -0,0 +1,17 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from ....response_handlers.base_handler import BaseHandler
from .lsp_event import LspEvent
@dataclass
class RegisterLspClientEvent(LspEvent):
lang_id: str = ""
lang_config: str = "{}"
handler: BaseHandler = None

View File

@@ -0,0 +1,13 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from .lsp_event import LspEvent
@dataclass
class UnregisterLspClientEvent(LspEvent):
lang_id: str = ""

View File

@@ -0,0 +1,128 @@
# Python imports
# Lib imports
# Application imports
from libs.controllers.controller_base import ControllerBase
from libs.event_factory import Event_Factory, Code_Event_Types
from .dto.code.events import \
RegisterLspClientEvent, UnregisterLspClientEvent
from .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_manager_client import LSPManagerClient
from .response_handlers.response_registry import ResponseRegistry
class LSPManager(ControllerBase):
def __init__(self):
super(LSPManager, 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_manager_client: LSPManagerClient = LSPManagerClient()
self.response_registry: ResponseRegistry = ResponseRegistry()
def _load_widgets(self):
self.lsp_manager_ui: LSPManagerUI = LSPManagerUI()
self.lsp_manager_ui.connect('create-client', self._on_create_client)
self.lsp_manager_ui.connect('close-client', self._on_close_client)
def _do_bind_mapping(self):
self.response_cache.set_lsp_manager_client(self.lsp_manager_client)
self.provider.response_cache = self.response_cache
self.response_registry.set_event_hub(
self.emit, self.emit_to, self.provider
)
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.RegisterLspClientEvent):
self.response_registry.register_handler(event.lang_id, event.handler)
self.lsp_manager_ui.add_client_listing(event.lang_id, event.lang_config)
elif isinstance(event, Code_Event_Types.UnregisterLspClientEvent):
self.response_registry.unregister_handler(event.lang_id)
self.lsp_manager_ui.remove_client_listing(event.lang_id)
def _on_create_client(self, ui, lang_id: str, workspace_uri: str) -> bool:
init_opts = ui.get_init_opts(lang_id)
result = self.create_client(lang_id, workspace_uri, init_opts)
if result:
ui.toggle_client_buttons(show_close=True)
return result
def _on_close_client(self, ui, lang_id: str) -> bool:
result = self.close_client(lang_id)
if result:
ui.toggle_client_buttons(show_close=False)
return result
def handle_destroy(self):
self.lsp_manager_ui.disconnect_by_func(self._on_create_client)
self.lsp_manager_ui.disconnect_by_func(self._on_close_client)
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {}
) -> bool:
client = self.lsp_manager_client.create_client(
lang_id, workspace_uri, init_opts
)
handler = self.response_registry.get_handler(lang_id)
self.lsp_manager_client.active_language_id = lang_id
if not client or not handler:
logger.error(f"LSP Manager: Either 'client' or 'handler' didn't get created...'")
self.close_client(lang_id)
return False
handler.set_context(self.response_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_manager_client.close_client(lang_id)
self.response_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_manager_client.active_language_id in self.lsp_manager_client.clients:
logger.debug(f"No LSP client for '{self.lsp_manager_client.active_language_id}', skipping 'server_response'")
return
controller = self.lsp_manager_client.get_active_client()
event = controller.get_event_by_id(lsp_response.id)
handler = self.response_registry.get_handler(
self.lsp_manager_client.active_language_id, event
)
if not handler: return
handler.handle(event, lsp_response.result, controller)
elif isinstance(lsp_response, LSPResponseNotification):
handler = self.response_registry.get_handler("default", lsp_response.method)
if not handler: return
handler.set_context(self.response_registry)
handler.set_response_cache(self.response_cache)
handler.handle(lsp_response.method, lsp_response.params, 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 .client.lsp_client import LSPClient
class LSPManagerClient(LSPClientEventsMixin):
def __init__(self):
super(LSPManagerClient, 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 = {}
) -> LSPClient:
if lang_id in self.clients: return None
address = "127.0.0.1"
port = 9999
uri = f"ws://{address}:{port}/{lang_id}"
client = LSPClient()
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) -> LSPClient:
return self.clients[self.active_language_id]

View File

@@ -0,0 +1,227 @@
# Python imports
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import GtkSource
# Application imports
class LSPManagerUI(Gtk.Dialog):
__gsignals__ = {
'create-client': (GObject.SignalFlags.RUN_LAST, None, (str, str)),
'close-client': (GObject.SignalFlags.RUN_LAST, None, (str,)),
}
def __init__(self):
super(LSPManagerUI, self).__init__()
self.client_configs: dict[str, str] = {}
self.source_view = None
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_modal(True)
self.set_decorated(False)
self.set_vexpand(True)
self.set_hexpand(True)
def _setup_signals(self):
self.connect("show", self._handle_show)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
def _load_widgets(self):
content_area = self.get_content_area()
self.main_box = Gtk.Grid()
self.path_entry = Gtk.SearchEntry()
self.path_bttn = Gtk.FileChooserButton.new(
title = "Workspace Folder",
action = Gtk.FileChooserAction.SELECT_FOLDER
)
self.combo_box = Gtk.ComboBoxText()
self.hide_bttn = Gtk.Button(label = "X")
bttn_box = Gtk.Box()
self.create_client_bttn = Gtk.Button(label = "Create Language Client")
self.close_client_bttn = Gtk.Button(label = "Close Language Client")
self.path_entry.set_can_focus(False)
self.path_entry.set_placeholder_text("Workspace Folder...")
self.path_entry.connect("changed", self._path_changed, bttn_box)
self.path_bttn.set_halign(Gtk.Align.FILL)
self.path_bttn.connect("file-set", self._file_set)
self.combo_box.connect("changed", self._on_combo_changed)
self.hide_bttn_id = self.hide_bttn.connect("clicked", lambda widget: self.hide())
self.create_client_bttn.connect("clicked", self._create_client, self.close_client_bttn)
self.close_client_bttn.connect("clicked", self._close_client, self.create_client_bttn)
self.main_box.set_column_spacing(15)
self.main_box.set_row_spacing(15)
bttn_box.pack_start(self.create_client_bttn, False, False, 0)
bttn_box.pack_start(self.close_client_bttn, False, False, 0)
self.main_box.attach(child = self.path_entry, left = 0, top = 0, width = 4, height = 1)
self.main_box.attach(child = self.path_bttn, left = 4, top = 0, width = 1, height = 1)
self.main_box.attach(child = self.combo_box, left = 5, top = 0, width = 1, height = 1)
self.main_box.attach(child = self.hide_bttn, left = 6, top = 0, width = 1, height = 1)
self.main_box.attach(child = bttn_box, left = 0, top = 1, width = 1, height = 1)
content_area.set_vexpand(True)
content_area.set_hexpand(True)
content_area.add(self.main_box)
content_area.show_all()
self.close_client_bttn.hide()
bttn_box.hide()
def _handle_show(self, widget):
GLib.idle_add(self.path_entry.grab_focus)
def _handle_destroy(self, widget):
self.disconnect_by_func(self._show)
self.disconnect_by_func(self._handle_destroy)
self.path_bttn.disconnect_by_func(self._file_set)
self.combo_box.disconnect_by_func(self._on_combo_changed)
self.hide_bttn.disconnect(self.hide_bttn_id)
self.create_client_bttn.disconnect_by_func(self._create_client)
self.close_client_bttn.disconnect_by_func(self._close_client)
def _map_resize(self, widget, parent):
parent_x, parent_y = parent.get_position()
parent_width, parent_height = parent.get_size()
if parent_width == 0 or parent_height == 0: return
width = int(parent_width * 0.75)
height = int(parent_height * 0.75)
widget.resize(width, height)
x = parent_x + (parent_width - width) // 2
y = parent_y + (parent_height - height) // 2
widget.move(x, y)
def _path_changed(self, widget, buttons_widget):
if not widget.get_text():
self.path_bttn.unselect_all()
self.path_bttn.emit("file-set")
buttons_widget.hide()
return
self.set_source_view_text( self.path_entry.get_text() )
buttons_widget.show()
def _file_set(self, widget):
fname = widget.get_filename()
fname = "" if not fname else fname
self.path_entry.set_text(fname)
lang_id = self.combo_box.get_active_text()
if not lang_id or lang_id not in self.client_configs: return
self.set_source_view_text(
"{workspace.folder}" if not fname else fname
)
def _create_client(self, widget, sibling):
if not self.source_view: return
buffer = self.source_view.get_buffer()
lang_id = self.combo_box.get_active_text()
if not lang_id: return
workspace_dir = self.path_entry.get_text()
self.emit('create-client', lang_id, workspace_dir)
def _close_client(self, widget, sibling):
lang_id = self.combo_box.get_active_text()
if not lang_id: return
self.emit('close-client', lang_id)
def _on_combo_changed(self, combo: Gtk.ComboBoxText):
lang_id = combo.get_active_text()
self.set_source_view_text( self.path_entry.get_text() )
def set_source_view_text(self, workspace_dir: str):
lang_id = self.combo_box.get_active_text()
if not lang_id: return
json_str = self.client_configs[lang_id].replace("{workspace.folder}", workspace_dir)
buffer = self.source_view.get_buffer()
buffer.set_text(json_str, -1)
def map_parent_resize_event(self, parent):
self.size_allocate_id = parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent))
def unmap_parent_resize_event(self, parent):
parent.disconnect(self.size_allocate_id)
def set_source_view(self, source_view):
scrolled_win = Gtk.ScrolledWindow()
lang_manager = GtkSource.LanguageManager()
buffer = source_view.get_buffer()
language = lang_manager.get_language("json")
self.source_view = source_view
buffer.set_language(language)
buffer.set_style_scheme(self.source_view.syntax_theme)
scrolled_win.set_hexpand(True)
scrolled_win.set_vexpand(True)
scrolled_win.add(self.source_view)
self.main_box.attach(child = scrolled_win, left = 0, top = 2, width = 7, height = 1)
scrolled_win.show_all()
def add_client_listing(self, lang_id: str, lang_config: str):
self.combo_box.append_text(lang_id)
self.client_configs[lang_id] = lang_config
def remove_client_listing(self, lang_id: str):
model = self.combo_box.get_model()
for i, row in enumerate(model):
if row[0] == lang_id: # assuming text is in column 0
self.combo_box.remove(i)
break
if lang_id in self.client_configs:
del self.client_configs[lang_id]
def get_init_opts(self, lang_id: str) -> dict:
if not lang_id or lang_id not in self.client_configs: return {}
try:
lang_config = json.loads(self.client_configs[lang_id])
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON for {lang_id}: {e}")
return {}
return lang_config.get("initialization-options", {})
def toggle_client_buttons(self, show_close: bool):
self.create_client_bttn.set_visible(not show_close)
self.close_client_bttn.set_visible(show_close)

View File

@@ -3,5 +3,6 @@
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

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

View File

@@ -1,6 +1,9 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
@@ -8,6 +11,7 @@ from libs.dto.states import SourceViewStates
from plugins.plugin_types import PluginCode
from .dto.code import events as lsp_events
from .lsp_manager import LSPManager
@@ -25,9 +29,13 @@ class Plugin(PluginCode):
...
def load(self):
Event_Factory.register_events( lsp_events.__dict__.items() )
self.register_controller("lsp_manager", lsp_manager)
window = self.request_ui_element("main-window")
lsp_manager.map_parent_resize_event(window)
lsp_manager.lsp_manager_ui.map_parent_resize_event(window)
event = Event_Factory.create_event("register_command",
command_name = "LSP Manager",
@@ -37,7 +45,7 @@ class Plugin(PluginCode):
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(
event = Event_Factory.create_event(
"register_provider",
provider_name = "LSP Completer",
provider = lsp_manager.provider,
@@ -52,14 +60,32 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
source_view = event.response
lsp_manager.load_lsp_servers_config()
lsp_manager.set_source_view(source_view)
lsp_manager.load_lsp_servers_config_placeholders()
lsp_manager.provider.response_cache._prompt_completion_request = \
self._prompt_completion_request
lsp_manager.lsp_manager_ui.set_source_view(source_view)
lsp_manager.provider.response_cache._prompt_goto_request = \
self._prompt_goto_request
def unload(self):
Event_Factory.unregister_events( lsp_events.__dict__.items() )
self.unregister_controller("lsp_manager")
window = self.request_ui_element("main-window")
lsp_manager.lsp_manager_ui.unmap_parent_resize_event(window)
event = Event_Factory.create_event("unregister_command",
command_name = "LSP Manager",
command = Handler,
binding_mode = "released",
binding = ["<Shift><Control>l", "<Control>g", "<Control>i"]
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "LSP Completer"
)
self.emit_to("completion", event)
lsp_manager.handle_destroy()
def run(self):
...
@@ -67,29 +93,6 @@ class Plugin(PluginCode):
def generate_plugin_element(self):
...
def _prompt_completion_request(self):
event = Event_Factory.create_event(
"get_active_view",
)
self.emit_to("source_views", event)
view = event.response
event = Event_Factory.create_event(
"request_completion",
view = view,
provider = lsp_manager.provider
)
self.emit_to("completion", event)
def _prompt_goto_request(self, uri: str):
event = Event_Factory.create_event(
"get_active_view",
)
self.emit_to("source_views", event)
view = event.response
view._on_uri_data_received( [uri] )
class Handler:
@staticmethod
@@ -109,7 +112,7 @@ class Handler:
column = iter.get_line_offset()
if char_str == "g":
lsp_manager.provider.response_cache.process_goto_definition(
lsp_manager.lsp_manager_client.process_goto_definition(
file.ftype, file.fpath, line, column
)
@@ -118,4 +121,4 @@ class Handler:
if char_str == "i":
return
lsp_manager.hide() if lsp_manager.is_visible() else lsp_manager.show()
lsp_manager.lsp_manager_ui.hide() if lsp_manager.lsp_manager_ui.is_visible() else lsp_manager.lsp_manager_ui.show()

View File

@@ -0,0 +1,2 @@
from .provider import Provider
from .provider_response_cache import ProviderResponseCache

View File

@@ -22,7 +22,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
def __init__(self):
super(Provider, self).__init__()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
self.response_cache: ProviderResponseCache = None
def pre_populate(self, context):
@@ -32,13 +32,19 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
return "LSP Code Completion"
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.backward_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...
if not (ch in ('_', '.', ' ') or ch.isalnum()):
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
if not (ch in ('_', '.') or ch.isalnum()):
return False
buffer = iter.get_buffer()
@@ -60,9 +66,8 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
def do_get_activation(self):
""" The context for when a provider will show results """
# return GtkSource.CompletionActivation.NONE
# return GtkSource.CompletionActivation.USER_REQUESTED
return GtkSource.CompletionActivation.USER_REQUESTED
# return GtkSource.CompletionActivation.INTERACTIVE
return GtkSource.CompletionActivation.INTERACTIVE | GtkSource.CompletionActivation.USER_REQUESTED
def do_populate(self, context):
results = self.response_cache.filter_with_context(context)

View File

@@ -0,0 +1,44 @@
# Python imports
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self):
super(ProviderResponseCache, self).__init__()
self.matchers: dict = {}
self.lsp_manager_client = None
def set_lsp_manager_client(self, lsp_client):
self.lsp_manager_client = lsp_client
def process_file_load(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_load(event)
def process_file_close(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_close(event)
def process_file_save(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_save(event)
def process_file_change(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_change(event)
def filter(self, word: str) -> list[dict]:
return []
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
return list( self.matchers.values() )

View File

@@ -0,0 +1,3 @@
from .base_handler import BaseHandler
from .default import DefaultHandler
from .response_registry import ResponseRegistry

View File

@@ -0,0 +1,30 @@
# Python imports
# 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,134 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .base_handler 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._prompt_completion_request()
def _handle_definition(self, response, controller):
if not response: return
uri = response[0]["uri"]
self._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")
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):
event = Event_Factory.create_event("get_active_view")
self.emit_to("source_views", event)
view = event.response
event = Event_Factory.create_event(
"request_completion",
view = view,
provider = self.context._provider
)
self.emit_to("completion", event)

View File

@@ -0,0 +1,52 @@
# Python imports
# Lib imports
# Application imports
from .base_handler import BaseHandler
from .default import DefaultHandler
class ResponseRegistry:
def __init__(self):
self._instances: dict = {}
self._lang_handlers: dict = {
"default": DefaultHandler
}
def set_event_hub(self, emit, emit_to, provider = None):
self.emit = emit
self.emit_to = emit_to
self._provider = provider
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 unregister_handler(self, lang_id: str):
del self._lang_handlers[lang_id]
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,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

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

View File

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

View File

@@ -0,0 +1,100 @@
{
"info": "https://github.com/python-lsp/python-lsp-server",
"command": "lsp-ws-proxy -- pylsp",
"alt-command": "pylsp",
"alt-command2": "lsp-ws-proxy --listen 4114 -- pylsp",
"alt-command3": "pylsp --ws --port 4114",
"socket": "ws://127.0.0.1:9999/python",
"socket-two": "ws://127.0.0.1:9999/?name=pylsp",
"initialization-options": {
"pylsp": {
"rope": {
"ropeFolder": "{user.home}/.config/newton/lsps/ropeproject"
},
"plugins": {
"ruff": {
"enabled": true,
"extendSelect": ["I"],
"lineLength": 80
},
"pycodestyle": {
"enabled": false
},
"pyflakes": {
"enabled": false
},
"pylint": {
"enabled": true
},
"mccabe": {
"enabled": false
},
"pylsp_rope": {
"rename": false
},
"rope_rename": {
"enabled": false
},
"rope_autoimport": {
"enabled": true
},
"rope_completion": {
"enabled": false,
"eager": false
},
"jedi_rename": {
"enabled": true
},
"jedi_completion": {
"enabled": true,
"include_class_objects": true,
"include_function_objects": true,
"fuzzy": false
},
"jedi": {
"root_dir": "file://{workspace.folder}",
"extra_paths": [
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
]
}
}
}
}
},
"python - jedi-language-server": {
"hidden": true,
"info": "https://pypi.org/project/jedi-language-server/",
"command": "jedi-language-server",
"alt-command": "lsp-ws-proxy --listen 3030 -- jedi-language-server",
"socket": "ws://127.0.0.1:9999/python",
"socket-two": "ws://127.0.0.1:9999/?name=jedi-language-server",
"initialization-options": {
"jediSettings": {
"autoImportModules": [],
"caseInsensitiveCompletion": true,
"debug": false
},
"completion": {
"disableSnippets": false,
"resolveEagerly": false,
"ignorePatterns": []
},
"markupKindPreferred": "markdown",
"workspace": {
"extraPaths": [
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
],
"environmentPath": "{user.home}/Portable_Apps/py-venvs/gtk-apps-venv/venv/bin/python",
"symbols": {
"ignoreFolders": [
".nox",
".tox",
".venv",
"__pycache__",
"venv"
],
"maxSymbols": 20
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "Python LSP Client",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

@@ -0,0 +1,43 @@
# Python imports
from os import path
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .response_handler import PythonHandler
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
dirPth = path.dirname( path.realpath(__file__) )
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
config = f.read()
event = Event_Factory.create_event("register_lsp_client",
lang_id = "python",
lang_config = config,
handler = PythonHandler
)
self.emit_to("lsp_manager", event)
def unload(self):
event = Event_Factory.create_event("unregister_lsp_client",
lang_id = "python"
)
self.emit_to("lsp_manager", event)
def run(self):
...

Some files were not shown because too many files have changed in this diff Show More