21 Commits

Author SHA1 Message Date
70877a7ee1 feat(event-watchers): rework file state detection and migrate tree-sitter integration
Switch file state checks to trigger on FocusedViewEvent instead of TextChangedEvent
Resolve file via files controller and operate directly on file object
Add user prompt for externally modified files with optional reload
Auto-reload unmodified buffers when external changes are detected
Simplify file_is_deleted / file_is_externally_modified APIs

tree-sitter

Remove bundled tree_sitter_language_pack and related setup/notes scripts
Introduce local lightweight tree_sitter wrapper (Parser, get_parser)
Store parser per file and generate AST directly from buffer text
Remove runtime language download/config logic
Update manifest (drop autoload)

core

Simplify CLI file handling in BaseControllerMixin
Send directory args directly via IPC instead of encoding in file list

cleanup

Remove unused libs, scripts, and legacy code paths
2026-03-27 20:00:59 -05:00
cb73f6b3b0 Fix unload lifecycle, widget cleanup, and plugin removal handling
- Rename GodotHandler to GDScriptHandler in LSP client
- Fix unload() method naming in nanoesq_temp_buffer plugin
- Return manifest_meta in manifest_manager for manual launch plugins
- Properly destroy tabs_widget, viewport, and scrolled_win on unload
- Refactor plugin removal to search all manifest lists and fix cleanup order
- Fix widget reference in plugins_ui (use child instead of box)
2026-03-23 23:05:20 -05:00
e6eaa1d83c Fixed file_history plugin after breakage from testing a pattern 2026-03-23 21:42:05 -05:00
62731ae766 Improve file loading robustness and drag-and-drop handling
- Add error handling for UTF-8 decoding to gracefully handle invalid bytes
- Refactor DnD target handling with explicit target list and proper context completion
- Remove completed TODO item for file open event emission
- Clean up memory by deleting file contents after decoding
2026-03-23 01:32:57 -05:00
b5cec0d049 Fix multi-select word movement for snake_case and add Ctrl+scroll zoom
- Implement snake_case-aware word movement that treats underscores as part of words
- Fix selection edge detection to only move when caret is at appropriate boundary
- Add INSERT state guards to line_up/down commands
- Add Ctrl+scroll zoom in/out functionality for text size
- Remove obsolete newton.zip archive
2026-03-22 23:42:28 -05:00
c821f30880 Added TODOs; made explicit that LSP Manager pre launch if autoload true/unset; added WIP tree_sitter plugin 2026-03-22 17:56:48 -05:00
13908d7ba7 Refactor file loading to detect external modifications and add reload capability
- Extract _load_data() method from load_path() for reuse in reload()
- Add is_extters externally_modified() to track file state changes (mtime/size)
- Replace load_bytes() with load_contents() for better error handling
- Add reload() method to re-read file from disk
- Fix file_state_watcher by properly detecting external changes
- Update TODO list (add Terminal plugin, mark file_state_watcher as fixed)
2026-03-22 00:35:11 -05:00
ec69d4db73 Addressed 'tabs_bar' TODO 2026-03-21 18:16:20 -05:00
511138316a Addressed Telescope TODO; enhanced Telescope search 2026-03-21 17:19:10 -05:00
d6e0823e21 Adding images to README file 2026-03-21 15:29:12 -05:00
54e7b58c24 Updated README 2026-03-21 14:16:23 -05:00
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
208 changed files with 4211 additions and 2971 deletions

View File

@@ -1,25 +1,13 @@
# Python-With-Gtk-Template # Newton
A template project for Python with Gtk applications. A Python + Gtk 3 based quasi-IDE.
### Requirements
* PyGObject (Gtk introspection library)
* pygobject-stubs (For actually getting pylsp or python-language-server to auto complete in LSPs. Do if GTK3 --no-cache-dir --config-settings=config=Gtk3,Gdk3,Soup2)
* pyxdg (Desktop ".desktop" file parser)
* setproctitle (Define process title to search and kill more easily)
* sqlmodel (SQL databases and is powered by Pydantic and SQLAlchemy)
### Note ### Note
* pyrightconfig.json can prompt IDEs that use pyright lsp on where imports are located- look at venvPath and venv. "venvPath" is parent path of "venv" where "venv" is just the name of the folder under the parent path that is the python created venv. [TODO](TODO.md)
* Move respetive sub folder content under user_config to the same places in Linux. Though, user/share/<app name> can go to ~/.config folder if prefered.
* In additiion, place the plugins folder in the same app folder you moved to /usr/share/<app name> or ~/.config/<app name> .
There are a "\<change_me\>" strings and files that need to be set according to your app's name located at:
* \_\_builtins\_\_.py
* user_config/bin/app_name
* user_config/usr/share/app_name
* user_config/usr/share/app_name/icons/app_name.png
* user_config/usr/share/app_name/icons/app_name-64x64.png
* user_config/usr/share/applications/app_name.desktop
### Images
For the user_config, after changing names and files, copy all content to their respective destinations. ![1 Newton default view.](images/pic1.png)
The logic follows Debian Dpkg packaging and its placement logic. ![2 Newton split pane view.](images/pic2.png)
![3 Newton search and replace shown.](images/pic3.png)
![4 Newton displaying inline colors.](images/pic4.png)
![5 Newton as transparent with youtube playing below it.](images/pic5.png)
![6 Newton with plugins menu show as well as markdown preview shown.](images/pic6.png)

19
TODO.md Normal file
View File

@@ -0,0 +1,19 @@
___
### Add
1. Add TreeSitter
1. Add Collapsable code blocks
1. Add Godot LSP Client
1. Add Terminal plugin
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 respective fields to UI
___
### Fix
- Fix on lsp client unload to close files lsp side and unload server endpoint
___

BIN
images/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
images/pic3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/pic4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/pic5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

BIN
images/pic6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

View File

@@ -39,6 +39,24 @@ class Plugin(PluginCode):
self.emit_to("source_views", event) 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): def run(self):
... ...

View File

@@ -27,7 +27,19 @@ class Plugin(PluginCode):
colorize.handle_colorize(event.buffer) colorize.handle_colorize(event.buffer)
def load(self): 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_name = "tggle_colorize",
command = Handler, command = Handler,
binding_mode = "released", binding_mode = "released",
@@ -36,6 +48,7 @@ class Plugin(PluginCode):
self.emit_to("source_views", event) self.emit_to("source_views", event)
def run(self): def run(self):
... ...

View File

@@ -36,6 +36,16 @@ class Plugin(PluginCode):
self.emit_to("source_views", event) 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): 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

@@ -1,5 +1,5 @@
{ {
"name": "LSP Manager", "name": "File History",
"author": "ITDominator", "author": "ITDominator",
"version": "0.0.1", "version": "0.0.1",
"support": "", "support": "",

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.fpath)
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(
[
f"file://{history.pop()}"
]
)

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 unload(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) 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): def run(self):
... ...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
) )
self.emit_to("completion", event) 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): def run(self):
... ...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
) )
self.emit_to("completion", event) 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): def run(self):
... ...

View File

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

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
) )
self.emit_to("completion", event) 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): def run(self):
... ...

View File

@@ -35,5 +35,15 @@ class Plugin(PluginCode):
) )
self.emit_to("completion", event) 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): 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

@@ -17,16 +17,28 @@ class Plugin(PluginCode):
def _controller_message(self, event: Code_Event_Types.CodeEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.TextChangedEvent): if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
event.file.check_file_on_disk() event = Event_Factory.create_event(
"get_file", buffer = event.view.get_buffer()
)
self.emit_to("files", event)
if event.file.is_deleted(): file = event.response
file_is_deleted(event) if not file: return
elif event.file.is_externally_modified(): if file.ftype == "buffer": return
file_is_externally_modified(event)
file.check_file_on_disk()
if file.is_deleted():
file_is_deleted(file, self.emit)
elif file.is_externally_modified():
file_is_externally_modified(file, self.emit)
def load(self): def load(self):
... ...
def unload(self):
...
def run(self): def run(self):
... ...

View File

@@ -10,23 +10,45 @@ from libs.event_factory import Event_Factory, Code_Event_Types
def file_is_deleted(event): def ask_yes_no(message):
event.file.was_deleted = True dialog = Gtk.MessageDialog(
parent = None,
flags = 0,
message_type = Gtk.MessageType.QUESTION,
buttons = Gtk.ButtonsType.YES_NO,
text = message,
)
dialog.set_title("Confirm")
response = dialog.run()
dialog.destroy()
return response == Gtk.ResponseType.YES
def file_is_deleted(file, emit):
file.was_deleted = True
event = Event_Factory.create_event( event = Event_Factory.create_event(
"file_externally_deleted", "file_externally_deleted",
file = event.file, file = file,
buffer = event.buffer buffer = file.buffer
) )
self.emit(event) emit(event)
def file_is_externally_modified(event): def file_is_externally_modified(file, emit):
# event = Event_Factory.create_event( event = Event_Factory.create_event(
# "file_externally_modified", "file_externally_modified",
# file = event.file, file = file,
# buffer = event.buffer buffer = file.buffer
# ) )
# self.emit(event) emit(event)
... if not file.buffer.get_modified():
file.reload()
return
result = ask_yes_no("File has been externally modified. Reload?")
if not result: return
file.reload()

View File

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

View File

@@ -13,8 +13,6 @@ from gi.repository import Gtk
def add_prettify_json(buffer, menu): def add_prettify_json(buffer, menu):
menu.append( Gtk.SeparatorMenuItem() )
def on_prettify_json(menuitem, buffer): def on_prettify_json(menuitem, buffer):
start_itr, \ start_itr, \
end_itr = buffer.get_start_iter(), buffer.get_end_iter() end_itr = buffer.get_start_iter(), buffer.get_end_iter()

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
from .libs.tree_sitter_language_pack import \
init, download, get_language, get_parser, available_languages, process, ProcessConfig
# Optional: Pre-download specific languages for offline use
#init(["python", "javascript", "rust"])
# Get a language (auto-downloads if not cached)
language = get_language("python")
# Get a pre-configured parser (auto-downloads if needed)
parser = get_parser("python")
tree = parser.parse(b"def hello(): pass")
print(tree.root_node)
# List all available languages
for lang in available_languages():
print(lang)
# Extract file intelligence (auto-downloads language if needed)
result = process(
"def hello(): pass",
ProcessConfig( language = "python")
)
print(f"Functions: {len(result['structure'])}")
# Pre-download languages for offline use
download(["python", "javascript"])
# With chunking
result = process(
source,
ProcessConfig(
language = "python",
chunk_max_size = 1000,
comments = True
)
)
print(f"Chunks: {len(result['chunks'])}")

View File

@@ -0,0 +1,134 @@
#!/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
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TOOLS="$ROOT/.tools"
NODE_DIR="$TOOLS/node"
GRAMMARS_DIR="$ROOT/grammars"
BUILD_DIR="$ROOT/build"
OUTPUT="$ROOT/compiled"
NODE_VERSION="v24.14.1"
NODE_DIST="node-$NODE_VERSION-linux-x64"
NODE_ARCHIVE="$TOOLS/node.tar.xz"
NODE_URL="https://nodejs.org/dist/$NODE_VERSION/$NODE_DIST.tar.xz"
TS_CLI_VERSION="0.22.6"
LANGS=(
tree-sitter-python
tree-sitter-javascript
tree-sitter-html
tree-sitter-css
tree-sitter-json
tree-sitter-java
tree-sitter-c
tree-sitter-cpp
tree-sitter-go
)
REPOS=(
https://github.com/tree-sitter/tree-sitter-python
https://github.com/tree-sitter/tree-sitter-javascript
https://github.com/tree-sitter/tree-sitter-html
https://github.com/tree-sitter/tree-sitter-css
https://github.com/tree-sitter/tree-sitter-json
https://github.com/tree-sitter/tree-sitter-java
https://github.com/tree-sitter/tree-sitter-c
https://github.com/tree-sitter/tree-sitter-cpp
https://github.com/tree-sitter/tree-sitter-go
)
mkdir -p "$TOOLS" "$GRAMMARS_DIR" "$BUILD_DIR" "$OUTPUT"
ensure_node() {
if [ -x "$NODE_DIR/bin/node" ]; then
echo "==> Using cached Node.js"
return
fi
echo "==> Downloading Node.js $NODE_VERSION"
wget -O "$NODE_ARCHIVE" "$NODE_URL"
echo "==> Extracting Node.js"
mkdir -p "$NODE_DIR"
tar -xf "$NODE_ARCHIVE" -C "$NODE_DIR" --strip-components=1
rm "$NODE_ARCHIVE"
echo "==> Node installed at $NODE_DIR"
}
ensure_tree_sitter() {
export PATH="$NODE_DIR/bin:$PATH"
TS="$TOOLS/node_modules/.bin/tree-sitter"
if [ -x "$TS" ]; then
echo "==> Using cached tree-sitter-cli"
return
fi
echo "==> Installing tree-sitter-cli"
cd "$TOOLS"
npm init -y >/dev/null 2>&1 || true
npm install tree-sitter-cli@$TS_CLI_VERSION
}
sync_grammars() {
echo "==> Syncing grammars"
for i in "${!LANGS[@]}"; do
NAME="${LANGS[$i]}"
REPO="${REPOS[$i]}"
TARGET="$GRAMMARS_DIR/$NAME"
if [ -d "$TARGET/.git" ]; then
echo "Updating $NAME"
git -C "$TARGET" pull --depth 1
else
echo "Cloning $NAME"
git clone --depth 1 "$REPO" "$TARGET"
fi
done
}
build_lib() {
echo "==> Building Tree-sitter library"
mkdir -p "$OUTPUT"
PARSER_SRC=()
INCLUDE_PATHS=()
for GRAMMAR in "${LANGS[@]/#/$GRAMMARS_DIR/}"; do
echo "==> Processing grammar $GRAMMAR"
PARSER_SRC+=("$GRAMMAR/src/parser.c")
if [[ -f "$GRAMMAR/src/scanner.c" ]]; then
PARSER_SRC+=("$GRAMMAR/src/scanner.c")
fi
INCLUDE_PATHS+=("-I$GRAMMAR/src")
done
gcc -shared -o "$OUTPUT/languages.so" "${PARSER_SRC[@]}" "${INCLUDE_PATHS[@]}" -fPIC
echo "==> Output: $OUTPUT/languages.so"
}
main() {
ensure_node
ensure_tree_sitter
sync_grammars
build_lib
}
main "$@"

View File

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

View File

@@ -0,0 +1,71 @@
"""Python bindings to the Tree-sitter parsing library."""
from typing import Protocol as _Protocol
from ._binding import (
Language,
LogType,
LookaheadIterator,
Node,
Parser,
Point,
Query,
QueryCursor,
QueryError,
Range,
Tree,
TreeCursor,
LANGUAGE_VERSION,
MIN_COMPATIBLE_LANGUAGE_VERSION,
)
LogType.__doc__ = "The type of a log message."
Point.__doc__ = "A position in a multi-line text document, in terms of rows and columns."
Point.row.__doc__ = "The zero-based row of the document."
Point.column.__doc__ = "The zero-based column of the document."
class QueryPredicate(_Protocol):
"""A custom query predicate that runs on a pattern."""
def __call__(self, predicate, args, pattern_index, captures):
"""
Parameters
----------
predicate : str
The name of the predicate.
args : list[tuple[str, typing.Literal['capture', 'string']]]
The arguments to the predicate.
pattern_index : int
The index of the pattern within the query.
captures : dict[str, list[Node]]
The captures contained in the pattern.
Returns
-------
``True`` if the predicate matches, ``False`` otherwise.
Tip
---
You don't need to create an actual class, just a function with this signature.
"""
__all__ = [
"Language",
"LogType",
"LookaheadIterator",
"Node",
"Parser",
"Point",
"Query",
"QueryCursor",
"QueryError",
"QueryPredicate",
"Range",
"Tree",
"TreeCursor",
"LANGUAGE_VERSION",
"MIN_COMPATIBLE_LANGUAGE_VERSION",
]

View File

@@ -0,0 +1,416 @@
from enum import IntEnum
from collections.abc import ByteString, Callable, Iterator, Sequence
from typing import Annotated, Any, Final, Literal, NamedTuple, Protocol, Self, final, overload
from typing_extensions import deprecated
class _SupportsFileno(Protocol):
def fileno(self) -> int: ...
class Point(NamedTuple):
row: int
column: int
class LogType(IntEnum):
PARSE: int
LEX: int
@final
class Language:
@overload
@deprecated("int argument support is deprecated")
def __init__(self, ptr: Annotated[int, "TSLanguage *"], /) -> None: ...
@overload
def __init__(self, ptr: Annotated[object, "TSLanguage *"], /) -> None: ...
@property
def name(self) -> str | None: ...
@property
def abi_version(self) -> int: ...
@property
def semantic_version(self) -> tuple[int, int, int] | None: ...
@deprecated("Use abi_version instead")
@property
def version(self) -> int: ...
@property
def node_kind_count(self) -> int: ...
@property
def parse_state_count(self) -> int: ...
@property
def field_count(self) -> int: ...
@property
def supertypes(self) -> tuple[int, ...]: ...
def subtypes(self, supertype: int, /) -> tuple[int, ...]: ...
def node_kind_for_id(self, id: int, /) -> str | None: ...
def id_for_node_kind(self, kind: str, named: bool, /) -> int | None: ...
def node_kind_is_named(self, id: int, /) -> bool: ...
def node_kind_is_visible(self, id: int, /) -> bool: ...
def node_kind_is_supertype(self, id: int, /) -> bool: ...
def field_name_for_id(self, field_id: int, /) -> str | None: ...
def field_id_for_name(self, name: str, /) -> int | None: ...
def next_state(self, state: int, id: int, /) -> int: ...
def lookahead_iterator(self, state: int, /) -> LookaheadIterator | None: ...
@deprecated("Use the Query() constructor instead")
def query(self, source: str, /) -> Query: ...
def copy(self) -> Language: ...
def __repr__(self) -> str: ...
def __eq__(self, other: Any, /) -> bool: ...
def __ne__(self, other: Any, /) -> bool: ...
def __hash__(self) -> int: ...
def __copy__(self) -> Language: ...
@final
class Node:
@property
def id(self) -> int: ...
@property
def kind_id(self) -> int: ...
@property
def grammar_id(self) -> int: ...
@property
def grammar_name(self) -> str: ...
@property
def type(self) -> str: ...
@property
def is_named(self) -> bool: ...
@property
def is_extra(self) -> bool: ...
@property
def has_changes(self) -> bool: ...
@property
def has_error(self) -> bool: ...
@property
def is_error(self) -> bool: ...
@property
def parse_state(self) -> int: ...
@property
def next_parse_state(self) -> int: ...
@property
def is_missing(self) -> bool: ...
@property
def start_byte(self) -> int: ...
@property
def end_byte(self) -> int: ...
@property
def byte_range(self) -> tuple[int, int]: ...
@property
def range(self) -> Range: ...
@property
def start_point(self) -> Point: ...
@property
def end_point(self) -> Point: ...
@property
def children(self) -> list[Node]: ...
@property
def child_count(self) -> int: ...
@property
def named_children(self) -> list[Node]: ...
@property
def named_child_count(self) -> int: ...
@property
def parent(self) -> Node | None: ...
@property
def next_sibling(self) -> Node | None: ...
@property
def prev_sibling(self) -> Node | None: ...
@property
def next_named_sibling(self) -> Node | None: ...
@property
def prev_named_sibling(self) -> Node | None: ...
@property
def descendant_count(self) -> int: ...
@property
def text(self) -> bytes | None: ...
def walk(self) -> TreeCursor: ...
def edit(
self,
start_byte: int,
old_end_byte: int,
new_end_byte: int,
start_point: Point | tuple[int, int],
old_end_point: Point | tuple[int, int],
new_end_point: Point | tuple[int, int],
) -> None: ...
def child(self, index: int, /) -> Node | None: ...
def named_child(self, index: int, /) -> Node | None: ...
def first_child_for_byte(self, byte: int, /) -> Node | None: ...
def first_named_child_for_byte(self, byte: int, /) -> Node | None: ...
def child_by_field_id(self, id: int, /) -> Node | None: ...
def child_by_field_name(self, name: str, /) -> Node | None: ...
def child_with_descendant(self, descendant: Node, /) -> Node | None: ...
def children_by_field_id(self, id: int, /) -> list[Node]: ...
def children_by_field_name(self, name: str, /) -> list[Node]: ...
def field_name_for_child(self, child_index: int, /) -> str | None: ...
def field_name_for_named_child(self, child_index: int, /) -> str | None: ...
def descendant_for_byte_range(
self,
start_byte: int,
end_byte: int,
/,
) -> Node | None: ...
def named_descendant_for_byte_range(
self,
start_byte: int,
end_byte: int,
/,
) -> Node | None: ...
def descendant_for_point_range(
self,
start_point: Point | tuple[int, int],
end_point: Point | tuple[int, int],
/,
) -> Node | None: ...
def named_descendant_for_point_range(
self,
start_point: Point | tuple[int, int],
end_point: Point | tuple[int, int],
/,
) -> Node | None: ...
def __repr__(self) -> str: ...
def __str__(self) -> str: ...
def __eq__(self, other: Any, /) -> bool: ...
def __ne__(self, other: Any, /) -> bool: ...
def __hash__(self) -> int: ...
@final
class Tree:
@property
def root_node(self) -> Node: ...
@property
def included_ranges(self) -> list[Range]: ...
@property
def language(self) -> Language: ...
def root_node_with_offset(
self,
offset_bytes: int,
offset_extent: Point | tuple[int, int],
/,
) -> Node | None: ...
def copy(self) -> Tree: ...
def edit(
self,
start_byte: int,
old_end_byte: int,
new_end_byte: int,
start_point: Point | tuple[int, int],
old_end_point: Point | tuple[int, int],
new_end_point: Point | tuple[int, int],
) -> None: ...
def walk(self) -> TreeCursor: ...
def changed_ranges(self, new_tree: Tree, /) -> list[Range]: ...
def print_dot_graph(self, file: _SupportsFileno, /) -> None: ...
def __copy__(self) -> Tree: ...
@final
class TreeCursor:
@property
def node(self) -> Node | None: ...
@property
def field_id(self) -> int | None: ...
@property
def field_name(self) -> str | None: ...
@property
def depth(self) -> int: ...
@property
def descendant_index(self) -> int: ...
def copy(self) -> TreeCursor: ...
def reset(self, node: Node, /) -> None: ...
def reset_to(self, cursor: TreeCursor, /) -> None: ...
def goto_first_child(self) -> bool: ...
def goto_last_child(self) -> bool: ...
def goto_parent(self) -> bool: ...
def goto_next_sibling(self) -> bool: ...
def goto_previous_sibling(self) -> bool: ...
def goto_descendant(self, index: int, /) -> None: ...
def goto_first_child_for_byte(self, byte: int, /) -> int | None: ...
def goto_first_child_for_point(self, point: Point | tuple[int, int], /) -> int | None: ...
def __copy__(self) -> TreeCursor: ...
@final
class Parser:
@overload
def __init__(
self,
language: Language | None = None,
*,
included_ranges: Sequence[Range] | None = None,
logger: Callable[[LogType, str], None] | None = None,
) -> None: ...
@deprecated("timeout_micros is deprecated")
@overload
def __init__(
self,
language: Language | None = None,
*,
included_ranges: Sequence[Range] | None = None,
timeout_micros: int | None = None,
logger: Callable[[LogType, str], None] | None = None,
) -> None: ...
@property
def language(self) -> Language | None: ...
@language.setter
def language(self, language: Language) -> None: ...
@language.deleter
def language(self) -> None: ...
@property
def included_ranges(self) -> list[Range]: ...
@included_ranges.setter
def included_ranges(self, ranges: Sequence[Range]) -> None: ...
@included_ranges.deleter
def included_ranges(self) -> None: ...
@deprecated("Use the progress_callback in parse()")
@property
def timeout_micros(self) -> int: ...
@deprecated("Use the progress_callback in parse()")
@timeout_micros.setter
def timeout_micros(self, timeout: int) -> None: ...
@deprecated("Use the progress_callback in parse()")
@timeout_micros.deleter
def timeout_micros(self) -> None: ...
@property
def logger(self) -> Callable[[LogType, str], None] | None: ...
@logger.setter
def logger(self, logger: Callable[[LogType, str], None]) -> None: ...
@logger.deleter
def logger(self) -> None: ...
@overload
def parse(
self,
source: ByteString,
/,
old_tree: Tree | None = None,
encoding: Literal["utf8", "utf16", "utf16le", "utf16be"] = "utf8",
) -> Tree: ...
@overload
def parse(
self,
read_callback: Callable[[int, Point], ByteString | None],
/,
old_tree: Tree | None = None,
encoding: Literal["utf8", "utf16", "utf16le", "utf16be"] = "utf8",
progress_callback: Callable[[int, bool], bool] | None = None,
) -> Tree: ...
def reset(self) -> None: ...
def print_dot_graphs(self, file: _SupportsFileno | None, /) -> None: ...
class QueryError(ValueError): ...
class QueryPredicate(Protocol):
def __call__(
self,
predicate: str,
args: list[tuple[str, Literal["capture", "string"]]],
pattern_index: int,
captures: dict[str, list[Node]],
) -> bool: ...
@final
class Query:
def __new__(cls, language: Language, source: str, /) -> Self: ...
def pattern_count(self) -> int: ...
def capture_count(self) -> int: ...
def string_count(self) -> int: ...
def start_byte_for_pattern(self, index: int, /) -> int: ...
def end_byte_for_pattern(self, index: int, /) -> int: ...
def is_pattern_rooted(self, index: int, /) -> bool: ...
def is_pattern_non_local(self, index: int, /) -> bool: ...
def is_pattern_guaranteed_at_step(self, index: int, /) -> bool: ...
def capture_name(self, index: int, /) -> str: ...
def capture_quantifier(
self,
pattern_index: int,
capture_index: int,
/
) -> Literal["", "?", "*", "+"]: ...
def string_value(self, index: int, /) -> str: ...
def disable_capture(self, name: str, /) -> None: ...
def disable_pattern(self, index: int, /) -> None: ...
def pattern_settings(self, index: int, /) -> dict[str, str | None]: ...
def pattern_assertions(self, index: int, /) -> dict[str, tuple[str | None, bool]]: ...
@final
class QueryCursor:
@overload
def __init__(self, query: Query, *, match_limit: int = 0xFFFFFFFF) -> None: ...
@deprecated("timeout_micros is deprecated")
@overload
def __init__(
self,
query: Query,
*,
match_limit: int = 0xFFFFFFFF,
timeout_micros: int = 0
) -> None: ...
@property
def match_limit(self) -> int: ...
@match_limit.setter
def match_limit(self, limit: int) -> None: ...
@match_limit.deleter
def match_limit(self) -> None: ...
@deprecated("Use the progress_callback in matches() or captures()")
@property
def timeout_micros(self) -> int: ...
@deprecated("Use the progress_callback in matches() or captures()")
@timeout_micros.setter
def timeout_micros(self, timeout: int) -> None: ...
@property
def did_exceed_match_limit(self) -> bool: ...
def set_max_start_depth(self, depth: int, /) -> None: ...
def set_byte_range(self, start: int, end: int, /) -> None: ...
def set_point_range(
self,
start: Point | tuple[int, int],
end: Point | tuple[int, int],
/,
) -> None: ...
def captures(
self,
node: Node,
predicate: QueryPredicate | None = None,
progress_callback: Callable[[int], bool] | None = None,
/,
) -> dict[str, list[Node]]: ...
def matches(
self,
node: Node,
predicate: QueryPredicate | None = None,
progress_callback: Callable[[int], bool] | None = None,
/,
) -> list[tuple[int, dict[str, list[Node]]]]: ...
@final
class LookaheadIterator(Iterator[tuple[int, str]]):
@property
def language(self) -> Language: ...
@property
def current_symbol(self) -> int: ...
@property
def current_symbol_name(self) -> str: ...
def reset(self, state: int, /, language: Language | None = None) -> bool: ...
def names(self) -> list[str]: ...
def symbols(self) -> list[int]: ...
def __next__(self) -> tuple[int, str]: ...
@final
class Range:
def __init__(
self,
start_point: Point | tuple[int, int],
end_point: Point | tuple[int, int],
start_byte: int,
end_byte: int,
) -> None: ...
@property
def start_point(self) -> Point: ...
@property
def end_point(self) -> Point: ...
@property
def start_byte(self) -> int: ...
@property
def end_byte(self) -> int: ...
def __eq__(self, other: Any, /) -> bool: ...
def __ne__(self, other: Any, /) -> bool: ...
def __repr__(self) -> str: ...
def __hash__(self) -> int: ...
LANGUAGE_VERSION: Final[int]
MIN_COMPATIBLE_LANGUAGE_VERSION: Final[int]

View File

@@ -0,0 +1,8 @@
{
"name": "Tree-sitter",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"pre_launch": true,
"requests": {}
}

View File

@@ -0,0 +1,47 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .tree_sitter import Parser, get_parser
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.TextChangedEvent):
if not hasattr(event.file, "tree_sitter"):
parser = get_parser( event.file.ftype )
if not parser: return
event.file.tree_sitter = parser
buffer = event.file.buffer
start_itr, \
end_itr = buffer.get_bounds()
text = buffer.get_text(start_itr, end_itr, True)
tree = event.file.tree_sitter.parse( text.encode("UTF-8") )
event.file.ast = tree
# root = tree.root_node
# print("Root type:", root.type)
# for child in root.children:
# print(child.type, child.start_point, child.end_point)
def load(self):
...
def unload(self):
...
def run(self):
...

View File

@@ -0,0 +1,57 @@
# Python imports
import os
import ctypes
# Lib imports
# Application imports
from .libs.tree_sitter import Language, Parser
_LIB_PATH = os.path.join(
os.path.dirname(__file__), "languages", "languages.so"
)
_LANGUAGE_LIB = ctypes.CDLL(_LIB_PATH)
def load_language(name: str) -> Language | None:
symbol = f"tree_sitter_{name}"
try:
func = getattr(_LANGUAGE_LIB, symbol)
except AttributeError:
logger.warning(f"Tree-sitter: {name} not found in 'languages.so' shared library...")
return None
func.restype = ctypes.c_void_p
return Language(func())
LANGUAGES = {
"python": load_language("python"),
"python3": load_language("python"),
"javascript": load_language("javascript"),
"html": load_language("html"),
"css": load_language("css"),
"json": load_language("json"),
"java": load_language("java"),
"c": load_language("c"),
"cpp": load_language("cpp"),
"go": load_language("go")
}
def get_parser(lang_name: str) -> Parser | None:
if not lang_name in LANGUAGES: return
language = LANGUAGES[lang_name]
if not language in LANGUAGES: return
parser = Parser()
parser.language = language
return parser

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 GDScriptHandler
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 = GDScriptHandler
)
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 .gdscript import GDScriptHandler

View File

@@ -0,0 +1,12 @@
# Python imports
# Lib imports
# Application imports
from lsp_manager.response_handlers.default import DefaultHandler
class GDScriptHandler(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,20 +1,23 @@
# Python imports # Python imports
import threading import threading
from os import path
import json
# Lib imports # Lib imports
import gi import gi
from gi.repository import GLib from gi.repository import GLib
# Application imports # Application imports
from libs.dto.code.lsp.lsp_messages import get_message_str from ..dto.code.lsp.lsp_messages import get_message_str
from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, ClientRequest, ClientNotification from ..dto.code.lsp.lsp_message_structs import \
from .lsp_controller_websocket import LSPControllerWebsocket LSPResponseTypes, ClientRequest, ClientNotification
from .lsp_client_websocket import LSPClientWebsocket
class LSPController(LSPControllerWebsocket): class LSPClient(LSPClientWebsocket):
def __init__(self): def __init__(self):
super(LSPController, self).__init__() super(LSPClient, self).__init__()
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers # https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers
# initialize-params-slim.json was created off of jedi_language_server one # initialize-params-slim.json was created off of jedi_language_server one
@@ -22,15 +25,13 @@ class LSPController(LSPControllerWebsocket):
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 +43,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 +52,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

@@ -3,12 +3,13 @@
# Lib imports # Lib imports
# Application imports # Application imports
from .lsp_controller_events import LSPControllerEvents from ..dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
from libs.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): def _send_message(self, data: ClientRequest or ClientNotification):
raise NotImplementedError raise NotImplementedError

View File

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

@@ -0,0 +1,9 @@
{
"name": "LSP Manager",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"pre_launch": true,
"autoload": false,
"requests": {}
}

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