12 Commits

Author SHA1 Message Date
0c3de1334a Fix line-based editing behavior for cut and duplicate commands
- Cut to temp buffer:
  - Respect selections by expanding to full line boundaries
  - Normalize end iterator to include full line + newline
  - Ensure consistent newline handling for last line
  - Preserve cursor position after delete
  - Prevent line merging when accumulating cut buffer

- Duplicate line:
  - Simplify logic using line-based iter APIs
  - Fix incorrect selection handling and off-by-one issues
  - Ensure full-line duplication for both selection and cursor cases
  - Correct cursor/selection restoration after duplication
2026-04-15 23:02:15 -05:00
41f3501e1f feat(code): improve comment toggling, terminal navigation, and editor event wiring
- Refactor Commenter toggle logic for line and multi-line comments
  - Preserve indentation and cursor position
  - Improve handling of existing comment detection and removal
  - Simplify bounds vs line comment dispatch

- Enhance terminal project navigation
  - Add project marker detection via Gio file traversal
  - Implement go-to-project-or-home behavior (Home key shortcut)
  - Automatically `cd` into detected project root or home directory
  - Wire terminal widget navigation through VteWidget

- Improve terminal integration
  - Pass emit_to into terminals view for event dispatching
  - Add ability for VteWidget to trigger project navigation

- Update split pane shortcut
  - Change close split view binding to Alt+\

- Add editor event support
  - Emit `text_insert` event from SourceFile on insert
  - Add new TextInsertEvent DTO and register in event system

- Misc cleanup
  - Improve imports and structure in terminals module
  - Add project marker list and filesystem traversal helpers
2026-04-15 01:54:56 -05:00
12b5fe7304 feat: improve LSP lifecycle, terminal widget, and code folding support
- LSP:
  - Add shutdown and exit request/notification handling
  - Send initialized notification after initialize response
  - Gracefully close clients with delayed shutdown via GLib timeout
  - Fix LSP WS server ↔ Godot LSP communication flow

- Terminal (VteWidget):
  - Switch to async spawn with full environment inheritance
  - Add PROMPT_COMMAND OSC7 support for cwd tracking
  - Improve UX: scrollback, no audible bell, auto scroll
  - Implement clipboard shortcuts, selection copy, middle-click paste
  - Track cwd changes and update UI label
  - Add proper signal wiring and cleanup on destroy

- Code folding:
  - Add fold support for JS, HTML, CSS, JSON, C, C++, Go
  - Reset fold state safely when AST or filetype is unavailable

- UI (Plugins dialog):
  - Improve dialog behavior (non-modal, centered, transient)
  - Add focus-out auto-hide and Ctrl+Shift+P shortcut

- Misc:
  - Add type hints in VTE widget
  - Update TODOs (remove completed items, add LSP comm fix)
  - Add terminal plugin scaffolding
2026-04-13 00:50:42 -05:00
d8e0185d1c Refactor LSP manager architecture and event + completion pipeline
- Replace legacy LSPManagerClient + LSPManagerUI with ClientManager and UIManager
- Remove WebsocketClient in favor of unified Websocket implementation
- Move LSP initialization config loading into centralized config module
- Update LSPClient to support dynamic socket, improved init params, and doc version tracking
- Introduce range-based didChange notifications and add implementation + references requests
- Expand LSPClientEvents to support implementation/references and structured range edits
- Simplify websocket response handling and normalize LSP response parsing flow
- Decouple UI from LSP manager core; UI now emits address/port for client creation
- Refactor completion provider pipeline:
  - Split TextChangedEvent into TextInsertedEvent and DeleteRangeEvent
  - Update ProviderResponseCacheBase and controller dispatch paths accordingly
- Improve SourceBuffer and SourceFile event tracking with delete-range support
- Update plugin system:
  - Centralize command/provider registration via helper methods
  - Add LSP commands: definition, references, implementation, toggle UI
- Enhance response handlers to support references and implementation hooks
- Improve Python LSP config (jedi completion, signatures, references enabled)
- Fix minor GTK lifecycle and buffer signal handling issues
- Clean up unused imports, dead code, and outdated JSON server configs
2026-04-11 15:32:42 -05:00
fd1f5b8d64 refactor: remove split_pane_manager plugin and harden view handling
- Delete deprecated split_pane_manager command plugin and all related commands
- Introduce new split_pane command structure (untracked replacement)
- Add guard in code_minimap to handle missing active view
- Prevent language detection for non-file buffers in command_helpers

This cleans up legacy split pane logic and improves stability for edge cases.
2026-04-04 23:19:35 -05:00
69e766dc28 feat(editor): introduce split pane manager plugin and refactor source view system
* address TODO item for split_pane_manager plugin and bound keys
* add split_pane_manager plugin with create/close split view commands
* move sibling focus/move commands from core into plugin system
* remove legacy toggle_source_view plugin
* redesign EditorsContainer to use simpler Box-based layout
* refactor source view API to return (scrolled_window, source_view)
* add source view lifecycle events (create/remove/removed)
* rename GetNewCommandSystemEvent → CreateCommandSystemEvent
* update CommandsController to support command system creation and cleanup
* ensure command systems are removed with their source views
* improve focus border handling across sibling chains
* update telescope integration for new source view structure
* clean up container imports and initialization logic
* remove old keybindings and align with new split pane workflow
2026-04-04 21:33:09 -05:00
5911f37449 refactor(command-system): replace legacy CommandSystem with SourceViewCommandSystem
- Remove CommandSystem and CommandSystemMixin
- Introduce SourceViewCommandSystem and shared libs/command_system
- Update CommandsController to use new command system
- Adjust package exports accordingly

feat(lsp): clean up and normalize LSP configurations

- Fix invalid JSON in Godot LSP config (processId -> null, formatting)
- Remove embedded jedi-language-server config from main Python LSP config
- Add separate jedi-lsp-server-config.json

refactor(markers): rename move_word -> move_along_word for clarity

- Update all usages in MarkerManager

chore: minor formatting and whitespace fixes
2026-04-04 14:18:32 -05:00
6e46279da4 refactor(cursor/multi-insert): unify movement logic and improve word navigation
* Extract `_proc_move` to centralize cursor and selection handling
* Rework multi-insert cursor movement and key handling (arrow/ctrl/shift/super)
* Add `ignore_leader` support to decouple leader cursor behavior
* Replace `move_word_snake_case` with improved `move_along_word` (fix '-' handling)
* Add `is_super` modifier support and clean up TODO
2026-03-30 00:36:52 -05:00
d90415bffc Fix: Improve code folding functionality and gutter rendering
- Updated folding actions and engine for more consistent behavior.
- Added helper function `is_fold_hidden()` to check fold visibility state.
- Improved gutter renderer to handle collapsible code blocks more reliably.
- Refined tag handling for invisible folds to prevent desync issues.
- Removed code fold Fix related entry in `TODO.md`.
2026-03-29 14:33:40 -05:00
62a866d9bb feat(tree-sitter, views): initialize AST on focus and emit source view creation event
- Add set_ast helper to centralize Tree-sitter parsing logic
- Parse and attach AST on FocusedViewEvent and TextChangedEvent
- Request file from buffer on view focus before parsing
- Fix parser guard condition in get_parser (handle missing language properly)

- Emit CreatedSourceViewEvent when a new source view is added
- Register CreatedSourceViewEvent in DTO exports

- Update TODO:
  - Remove completed collapsible code blocks task
  - Add fix note for code block icon desync issue

chore:
- Add scaffolding for code_fold UI plugin
- Add created_source_view_event DTO
2026-03-29 03:09:43 -05:00
dc2997ec16 feat(lsp, ui, core): refactor LSP initialization, improve config handling, and clean up TreeSitter
* LSP Client & Manager
  Refactored initialization flow to use internal state instead of passing params
  Added workspace_path and init_opts to LSPClient
  Simplified send_initialize_message() (no external args)
  Updated manager/client APIs to require explicit workspace_path and typed init_opts
  Improved client lifecycle handling and UI button toggling

* LSP Manager UI
  Added {user.home} substitution support in configs
  Use live editor buffer for JSON config parsing instead of static template
  Minor cleanup and formatting improvements

* Core Widgets
  Prevent external modification checks for buffer-only files in SourceFile
  Minor formatting cleanup in SourceView

* Plugins / Manifest
  Expanded manifest schema: added description and copyright, reordered fields

* Cleanup
  Removed TreeSitter compile script and TODO entry
  General code formatting and small consistency fixes

chore: remove unused TreeSitter tooling and related TODO entry
2026-03-28 16:14:04 -05:00
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
114 changed files with 3102 additions and 1730 deletions

10
TODO.md
View File

@@ -1,19 +1,15 @@
___
### 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
- Fix LSP WS Server to Godot LSP Server communication
- Fix <Ctrl\>z in multi-insert mode being funky. Insure updates happen on block level.
I.E, maybe push updates to queue to insure block undo/redo?
___

View File

@@ -13,54 +13,95 @@ class Commenter(CodeCommentTagsMixin):
def keyboard_tggl_comment(self, buffer):
language = buffer.get_language()
if language is None: return
language = buffer.get_language()
if not language: return
start_tag, end_tag = self.get_comment_tags(language)
# Note: Only handling line comment tag- no block comment option
if not start_tag and not end_tag: return
if not (start_tag or end_tag): return
bounds = buffer.get_selection_bounds()
if bounds:
self._bounds_comment(
start_tag, end_tag, bounds, buffer
)
else:
self._line_comment(start_tag, end_tag, buffer)
start_tag += " "
end_tag = end_tag or ""
bounds = buffer.get_selection_bounds()
def _line_comment(self, start_tag, end_tag, buffer):
start_itr = buffer.get_iter_at_mark( buffer.get_insert() ).copy()
end_itr = start_itr.copy()
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True)
text = text.replace(start_tag, "") if text.startswith(start_tag) else start_tag + text
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
def _bounds_comment(self, start_tag, end_tag, bounds, buffer):
start_itr, end_itr = bounds
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True)
text = "\n".join(
line.replace(start_tag, "") if line.startswith(start_tag) else start_tag + line
for line in text.splitlines()
(self._bounds_comment if bounds else self._line_comment)(
buffer, start_tag, end_tag, bounds
)
def _line_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start = buffer.get_iter_at_mark(buffer.get_insert()).copy()
end = start.copy()
line, col = start.get_line() + 1, start.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
text = buffer.get_text(start, end, True)
stripped = text.lstrip()
indent = text[:-len(stripped)] if stripped else text
if stripped.startswith(start_tag):
stripped = stripped[len(start_tag):].lstrip().replace(end_tag, "", 1)
else:
stripped = f"{start_tag}{stripped}{end_tag}"
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.delete(start, end)
buffer.insert(start, indent + stripped)
buffer.end_user_action()
buffer.place_cursor(buffer.get_iter_at_line_offset(line, col))
def _bounds_comment(self, buffer, start_tag: str, end_tag: str, bounds):
def indent_len(s): return len(s) - len(s.lstrip())
def insert(line, idx):
return f"{line[:idx]}{start_tag}{line[idx:]}{end_tag}"
def process(lines):
base_indent = min(
(indent_len(l) for l in lines if l.strip()),
default = 0
)
is_commented = all(
l.lstrip().startswith(start_tag)
for l in lines if l.strip()
)
if is_commented:
return [
l.replace(start_tag, "", 1).replace(end_tag, "", 1)
if l.lstrip().startswith(start_tag.lstrip())
else l
for l in lines
]
return [
l if not l.strip()
else insert(l, base_indent)
for l in lines
]
start, end = bounds
sline, scol = start.get_line(), start.get_line_offset()
eline, ecol = end.get_line(), end.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
lines = buffer.get_text(start, end, True).splitlines()
new_text = "\n".join(process(lines))
buffer.begin_user_action()
buffer.delete(start, end)
buffer.insert(start, new_text)
buffer.end_user_action()
buffer.select_range(
buffer.get_iter_at_line_offset(sline, scol),
buffer.get_iter_at_line_offset(eline, ecol),
)

View File

@@ -14,31 +14,44 @@ from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler:
@staticmethod
def execute(
view: GtkSource.View,
*args,
**kwargs
):
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)
if buffer.get_has_selection():
start_itr, end_itr = buffer.get_selection_bounds()
end_itr = start_itr.copy()
if not end_itr.forward_line():
end_itr = buffer.get_end_iter()
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
if not end_itr.is_end():
end_itr.forward_char()
else:
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
text = buffer.get_text(start_itr, end_itr, True)
if not text.endswith("\n"):
text += "\n"
view._cut_buffer += text
buffer.delete(start_itr, end_itr)
buffer.place_cursor(start_itr)
set_temp_cut_buffer_delayed(view)
set_temp_cut_buffer_delayed(view)

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import Gtk
from gi.repository import GtkSource
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
emit_to: callable = None
def get_source_view(widget):
if isinstance(widget, GtkSource.View):
return widget
if isinstance(widget, Gtk.ScrolledWindow):
return widget.get_child()
if isinstance(widget, Gtk.Paned):
return get_source_view(widget.get_child1())
return None
def execute(
source_view,
char_str,
modkeys_states
):
logger.debug("Command: Close Split Pane")
scrolled_win = source_view.get_parent()
pane = scrolled_win.get_parent()
if not isinstance(pane, Gtk.Paned): return
container = pane.get_parent()
source_view1 = pane.get_child1()
source_view2 = pane.get_child2()
if scrolled_win == source_view1:
remaining = source_view2
closing_view = source_view
else:
remaining = source_view1
closing_view = source_view
remaining_view = get_source_view(remaining)
left = closing_view.sibling_left
right = closing_view.sibling_right
if left:
left.sibling_right = right
if right:
right.sibling_left = left
pane.remove(source_view1)
pane.remove(source_view2)
container.remove(pane)
container.add(remaining)
event = Event_Factory.create_event(
"remove_source_view",
view = closing_view
)
emit_to("source_views", event)
remaining_view.grab_focus()

View File

@@ -0,0 +1,77 @@
# Python imports
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from libs.dto.states import SourceViewStates
emit_to: callable = None
def execute(
source_view1,
char_str,
modkeys_states
):
logger.debug("Command: Split Pane")
scrolled_win1 = source_view1.get_parent()
container = scrolled_win1.get_parent()
pane = Gtk.Paned()
event = Event_Factory.create_event(
"create_source_view",
state = SourceViewStates.INSERT
)
emit_to("source_views", event)
scrolled_win2, \
source_view2 = event.response
old_sibling_right = None
if source_view1.sibling_right:
old_sibling_right = source_view1.sibling_right
source_view1.sibling_right = source_view2
if old_sibling_right:
old_sibling_right.sibling_left = source_view2
source_view2.sibling_right = old_sibling_right
source_view2.sibling_left = source_view1
pane.set_hexpand(True)
pane.set_vexpand(True)
pane.set_wide_handle(True)
container.remove(scrolled_win1)
pane.pack1( scrolled_win1, True, True )
pane.pack2( scrolled_win2, True, True )
container.add(pane)
def _show(pane, alloc, is_vertical: bool):
if is_vertical:
pane.set_position(alloc.width / 2)
else:
pane.set_position(alloc.height / 2)
pane.disconnect(pane.show_id)
is_control, is_shift, is_alt = modkeys_states
alloc = container.get_allocation()
if char_str == "|":
pane.show_id = pane.connect("show", _show, alloc, True)
pane.set_orientation(Gtk.Orientation.VERTICAL)
elif char_str == "\\":
pane.show_id = pane.connect("show", _show, alloc, False)
pane.set_orientation(Gtk.Orientation.HORIZONTAL)
pane.show_all()
source_view2.command.exec("new_file")
source_view2.grab_focus()

View File

@@ -7,6 +7,7 @@ gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
@@ -18,5 +19,4 @@ def execute(
):
logger.debug("Command: Focus Left Sibling")
if not view.sibling_left: return
view.sibling_left.get_parent().show()
view.sibling_left.grab_focus()

View File

@@ -18,5 +18,4 @@ def execute(
):
logger.debug("Command: Focus Right Sibling")
if not view.sibling_right: return
view.sibling_right.get_parent().show()
view.sibling_right.grab_focus()

View File

@@ -1,5 +1,5 @@
{
"name": "Toggle Source View",
"name": "Split Pane",
"author": "ITDominator",
"version": "0.0.1",
"support": "",

View File

@@ -0,0 +1,100 @@
# Python imports
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
# Application imports
from plugins.plugin_types import PluginCode
from libs.event_factory import Event_Factory, Code_Event_Types
from . import create_split_view, \
close_split_view, \
focus_left_sibling, \
focus_right_sibling, \
move_to_left_sibling, \
move_to_right_sibling
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
gemit_to = self.emit_to
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
def _manage_signals(self, action: str):
_create_split_view = create_split_view
_close_split_view = close_split_view
_create_split_view.emit_to = self.emit_to
_close_split_view.emit_to = self.emit_to
event = Event_Factory.create_event(action,
command_name = "create_split_view",
command = _create_split_view,
binding_mode = "released",
binding = ["<Control>\\", "<Shift><Control>|"]
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "close_split_view",
command = _close_split_view,
binding_mode = "released",
binding = "<Alt>\\"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "focus_left_sibling",
command = focus_left_sibling,
binding_mode = "released",
binding = "<Control>Page_Up"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "focus_right_sibling",
command = focus_right_sibling,
binding_mode = "released",
binding = "<Control>Page_Down"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "move_to_left_sibling",
command = move_to_left_sibling,
binding_mode = "released",
binding = "<Control><Shift>Up"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "move_to_right_sibling",
command = move_to_right_sibling,
binding_mode = "released",
binding = "<Control><Shift>Down"
)
self.emit_to("source_views", event)
def run(self):
...

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
event = Event_Factory.create_event("register_command",
command_name = "toggle_source_view",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>h"
)
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):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: Toggle Source View")
target = view.get_parent()
target.hide() if target.is_visible() else target.show()
if view.sibling_left:
target = view.sibling_left.get_parent()
target.show()
view.sibling_left.grab_focus()
if view.sibling_right:
target = view.sibling_right.get_parent()
target.show()
view.sibling_right.grab_focus()

View File

@@ -51,7 +51,10 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
...
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
...
def filter(self, word: str) -> list[dict]:

View File

@@ -50,7 +50,10 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
...
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
...
def filter(self, word: str) -> list[dict]:

View File

@@ -49,7 +49,10 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
...
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
...
def filter(self, word: str) -> list[dict]:

View File

@@ -35,11 +35,14 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
buffer = event.file.buffer
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
buffer = event.buffer
self._clear_temp_delay()
self._set_temp_delay(buffer)
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
...
def _clear_temp_delay(self):
if self._temp_timeout_id:
GLib.source_remove(self._temp_timeout_id)

View File

@@ -17,13 +17,22 @@ class Plugin(PluginCode):
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.TextChangedEvent):
event.file.check_file_on_disk()
if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
event = Event_Factory.create_event(
"get_file", buffer = event.view.get_buffer()
)
self.emit_to("files", event)
if event.file.is_deleted():
file_is_deleted(event, self.emit)
elif event.file.is_externally_modified():
file_is_externally_modified(event, self.emit)
file = event.response
if not file: return
if file.ftype == "buffer": return
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):
...

View File

@@ -10,23 +10,45 @@ from libs.event_factory import Event_Factory, Code_Event_Types
def file_is_deleted(event, emit):
event.file.was_deleted = True
def ask_yes_no(message):
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(
"file_externally_deleted",
file = event.file,
buffer = event.buffer
file = file,
buffer = file.buffer
)
emit(event)
def file_is_externally_modified(event, emit):
# event = Event_Factory.create_event(
# "file_externally_modified",
# file = event.file,
# buffer = event.buffer
# )
# emit(event)
def file_is_externally_modified(file, emit):
event = Event_Factory.create_event(
"file_externally_modified",
file = file,
buffer = file.buffer
)
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

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

@@ -1,54 +0,0 @@
from typing import TypeAlias
from tree_sitter_language_pack._native import (
DownloadError,
LanguageNotFoundError,
ParseError,
ProcessConfig,
QueryError,
TreeHandle,
available_languages,
cache_dir,
clean_cache,
configure,
download,
download_all,
downloaded_languages,
get_binding,
get_language,
get_parser,
has_language,
init,
language_count,
manifest_languages,
parse_string,
process,
)
SupportedLanguage: TypeAlias = str
__all__ = [
"DownloadError",
"LanguageNotFoundError",
"ParseError",
"ProcessConfig",
"QueryError",
"SupportedLanguage",
"TreeHandle",
"available_languages",
"cache_dir",
"clean_cache",
"configure",
"download",
"download_all",
"downloaded_languages",
"get_binding",
"get_language",
"get_parser",
"has_language",
"init",
"language_count",
"manifest_languages",
"parse_string",
"process",
]

View File

@@ -1,276 +0,0 @@
from typing import Literal, TypeAlias
from tree_sitter import Language, Parser
class LanguageNotFoundError(ValueError): ...
class DownloadError(RuntimeError): ...
SupportedLanguage: TypeAlias = Literal[
"actionscript",
"ada",
"agda",
"apex",
"arduino",
"asm",
"astro",
"bash",
"batch",
"bazel",
"beancount",
"bibtex",
"bicep",
"bitbake",
"bsl",
"c",
"cairo",
"capnp",
"chatito",
"clarity",
"clojure",
"cmake",
"cobol",
"comment",
"commonlisp",
"cpon",
"cpp",
"css",
"csv",
"cuda",
"d",
"dart",
"diff",
"dockerfile",
"doxygen",
"dtd",
"elisp",
"elixir",
"elm",
"erlang",
"fennel",
"firrtl",
"fish",
"fortran",
"fsharp",
"fsharp_signature",
"func",
"gdscript",
"gitattributes",
"gitcommit",
"gitignore",
"gleam",
"glsl",
"gn",
"go",
"gomod",
"gosum",
"gradle",
"graphql",
"groovy",
"gstlaunch",
"hack",
"hare",
"haskell",
"haxe",
"hcl",
"heex",
"hlsl",
"html",
"hyprlang",
"ignorefile",
"ini",
"ispc",
"janet",
"java",
"javascript",
"jsdoc",
"json",
"jsonnet",
"julia",
"kconfig",
"kdl",
"kotlin",
"latex",
"linkerscript",
"lisp",
"llvm",
"lua",
"luadoc",
"luap",
"luau",
"magik",
"make",
"makefile",
"markdown",
"markdown_inline",
"matlab",
"mermaid",
"meson",
"netlinx",
"nim",
"ninja",
"nix",
"nqc",
"objc",
"ocaml",
"ocaml_interface",
"odin",
"org",
"pascal",
"pem",
"perl",
"pgn",
"php",
"pkl",
"po",
"pony",
"powershell",
"printf",
"prisma",
"properties",
"proto",
"psv",
"puppet",
"purescript",
"pymanifest",
"python",
"qmldir",
"qmljs",
"query",
"r",
"racket",
"re2c",
"readline",
"rego",
"requirements",
"ron",
"rst",
"ruby",
"rust",
"scala",
"scheme",
"scss",
"shell",
"smali",
"smithy",
"solidity",
"sparql",
"sql",
"squirrel",
"starlark",
"svelte",
"swift",
"tablegen",
"tcl",
"terraform",
"test",
"thrift",
"toml",
"tsv",
"tsx",
"twig",
"typescript",
"typst",
"udev",
"ungrammar",
"uxntal",
"v",
"verilog",
"vhdl",
"vim",
"vue",
"wast",
"wat",
"wgsl",
"xcompose",
"xml",
"yuck",
"zig",
]
class ParseError(RuntimeError): ...
class QueryError(ValueError): ...
class ProcessConfig:
language: str
structure: bool
imports: bool
exports: bool
comments: bool
docstrings: bool
symbols: bool
diagnostics: bool
chunk_max_size: int | None
def __init__(
self,
language: str,
*,
structure: bool = True,
imports: bool = True,
exports: bool = True,
comments: bool = True,
docstrings: bool = True,
symbols: bool = True,
diagnostics: bool = True,
chunk_max_size: int | None = None,
) -> None: ...
@staticmethod
def all(language: str) -> ProcessConfig: ...
@staticmethod
def minimal(language: str) -> ProcessConfig: ...
class TreeHandle:
def root_node_type(self) -> str: ...
def root_child_count(self) -> int: ...
def contains_node_type(self, node_type: str) -> bool: ...
def has_error_nodes(self) -> bool: ...
def to_sexp(self) -> str: ...
def error_count(self) -> int: ...
def root_node_info(self) -> dict[str, object]: ...
def find_nodes_by_type(self, node_type: str) -> list[dict[str, object]]: ...
def named_children_info(self) -> list[dict[str, object]]: ...
def extract_text(self, start_byte: int, end_byte: int) -> str: ...
def run_query(self, language: str, query_source: str) -> list[dict[str, object]]: ...
__all__ = [
"DownloadError",
"LanguageNotFoundError",
"ParseError",
"ProcessConfig",
"QueryError",
"SupportedLanguage",
"TreeHandle",
"available_languages",
"cache_dir",
"clean_cache",
"configure",
"download",
"download_all",
"downloaded_languages",
"get_binding",
"get_language",
"get_parser",
"has_language",
"init",
"language_count",
"manifest_languages",
"parse_string",
"process",
]
def get_binding(name: SupportedLanguage) -> object: ...
def get_language(name: SupportedLanguage) -> Language: ...
def get_parser(name: SupportedLanguage) -> Parser: ...
def available_languages() -> list[str]: ...
def has_language(name: str) -> bool: ...
def language_count() -> int: ...
def parse_string(language: str, source: str) -> TreeHandle: ...
def process(source: str, config: ProcessConfig) -> dict[str, object]: ...
def init(config: dict[str, object]) -> None: ...
def configure(*, cache_dir: str | None = None) -> None: ...
def download(names: list[str]) -> int: ...
def download_all() -> int: ...
def manifest_languages() -> list[str]: ...
def downloaded_languages() -> list[str]: ...
def clean_cache() -> None: ...
def cache_dir() -> str: ...

View File

@@ -4,6 +4,5 @@
"version": "0.0.1",
"support": "",
"pre_launch": true,
"autoload": false,
"requests": {}
}

View File

@@ -1,17 +0,0 @@
from .tree_sitter import Language
Language.build_library(
"my-languages.so",
[
"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",
"tree-sitter-gdscript",
],
)

View File

@@ -1,121 +0,0 @@
#!/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 old_build() {
touch __init__.py
cat <<'EOF' > compile.py
from tree_sitter import Language
Language.build_library(
"my-languages.so",
[
"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"
"tree-sitter-gdscript",
],
)
EOF
python compile.py
cd ..
}
function build() {
touch __init__.py
cat <<'EOF' > compile.py
from tree_sitter_language_pack import init, download, get_language, get_parser, available_languages
from tree_sitter_language_pack import 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'])}")
EOF
python compile.py
cd ..
}
function clone() {
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-python
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-javascript
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-html
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-css
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-json
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-java
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-c
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-cpp
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-go
# git clone --depth 1 https://github.com/godotengine/tree-sitter-gdscript
}
function setup() {
# pip install tree-sitter -t .
pip install tree-sitter-language-pack -t .
# pip install tree_sitter_languages -t . # Old unmaintained library
}
function main() {
cd "$(dirname "$0")"
echo "Working Dir: " $(pwd)
mkdir -p build
cd build
# clone "$@"
setup "$@"
build "$@"
}
main "$@";

View File

@@ -1,14 +1,4 @@
# Python imports
import sys
import os
BASE_DIR = os.path.dirname( os.path.realpath(__file__) )
LIBS_DIR = f"{BASE_DIR}/libs"
if str(LIBS_DIR) not in sys.path:
sys.path.insert(0, LIBS_DIR)
# Lib imports
@@ -17,7 +7,7 @@ from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
import tree_sitter_language_pack as tslp
from .tree_sitter import Parser, get_parser
@@ -26,52 +16,48 @@ class Plugin(PluginCode):
super(Plugin, self).__init__()
def set_ast(self, file):
if not hasattr(file, "tree_sitter"):
parser = get_parser( file.ftype )
if not parser: return
file.tree_sitter = parser
buffer = file.buffer
start_itr, \
end_itr = buffer.get_bounds()
text = buffer.get_text(start_itr, end_itr, True)
tree = file.tree_sitter.parse( text.encode("UTF-8") )
file.ast = tree
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.TextChangedEvent):
if not tslp.has_language( event.file.ftype ):
try:
tslp.download( [event.file.ftype] )
except Exception:
logger.info(
f"Tree Sitter Language Pack:\nCouldn't download -> '{event.file.ftype}' language type..."
)
return
buffer = event.file.buffer
start_itr, \
end_itr = buffer.get_bounds()
text = buffer.get_text(start_itr, end_itr, True)
result = tslp.process(
text,
tslp.ProcessConfig(
language = event.file.ftype
)
if isinstance(event, Code_Event_Types.FocusedViewEvent):
self.view = event.view
event = Event_Factory.create_event(
"get_file", buffer = self.view.get_buffer()
)
self.emit_to("files", event)
event.file.tree_sitter_meta = result
file = event.response
if not file: return
if file.ftype == "buffer": return
self.set_ast(file)
elif isinstance(event, Code_Event_Types.TextChangedEvent):
self.set_ast(event.file)
# 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):
tslp.configure(
cache_dir = f"{BASE_DIR}/cache/tree-sitter-language-pack/v1.0.0/libs"
)
...
def unload(self):
...
def run(self):
# tslp.download(
# [
# "python",
# "java",
# "go",
# "c",
# "cpp",
# "javascript",
# "html",
# "css",
# "json"
# ]
# )
# "gdscript"
...

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: return
parser = Parser()
parser.language = language
return parser

View File

@@ -7,46 +7,46 @@
"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
}
"processId": null,
"clientInfo": {
"name": "Godot",
"version": "4.4"
},
"textDocument": {
"synchronization": {
"dynamicRegistration": true,
"willSave": false,
"didSave": true,
"willSaveWaitUntil": false
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
"rootUri": "file://{workspace.folder}",
"capabilities": {
"workspace": {
"applyEdit": true,
"workspaceEdit": {
"documentChanges": true
}
},
"hover": {
"dynamicRegistration": true
},
"definition": {
"dynamicRegistration": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": 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"
},
"trace": "off"
}
}

View File

@@ -1,13 +1,9 @@
# Python imports
import threading
from os import path
import json
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from ..config import get_lsp_init_config
from ..dto.code.lsp.lsp_messages import get_message_str
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification
@@ -19,38 +15,30 @@ class LSPClient(LSPClientWebsocket):
def __init__(self):
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._socket: str = ""
self._language: str = ""
self._init_params: dict = {}
self._workspace_path: str = ""
self._message_id: int = -1
self._event_history: dict[int, str] = {}
self._init_params: dict = get_lsp_init_config()
self._init_opts: dict[str, str] = {}
self.doc_vers: dict[str, int] = {}
try:
_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)
except Exception as e:
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
self._message_id: int = -1
self._socket = None
self.read_lock = threading.Lock()
self.write_lock = threading.Lock()
def set_language(self, language: str):
self._language = language
def set_workspace_path(self, workspace_path: str):
self._workspace_path = workspace_path
def set_init_opts(self, init_opts: dict[str, str]):
self._init_opts = init_opts
def set_socket(self, socket: str):
self._socket = socket
def unset_socket(self):
self._socket = None
self._socket = ""
def send_notification(self, method: str, params: dict = {}):
self._send_message( ClientNotification(method, params) )
@@ -64,5 +52,5 @@ class LSPClient(LSPClientWebsocket):
if not message_id in self._event_history: return
return self._event_history[message_id]
def handle_lsp_response(self, lsp_response: LSPResponseTypes):
def handle_lsp_response(self, lsp_response: LSPResponseTypes | dict):
raise NotImplementedError

View File

@@ -9,19 +9,24 @@ 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 didchange_notification_range
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 implementation_request
from ..dto.code.lsp.lsp_messages import references_request
from ..dto.code.lsp.lsp_messages import symbols_request
from ..dto.code.lsp.lsp_messages import shutdown_request
from ..dto.code.lsp.lsp_messages import exit_request
class LSPClientEvents:
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str):
folder_name = os.path.basename(workspace_file)
def send_initialize_message(self):
folder_name = os.path.basename(self._workspace_path)
workspace_uri = f"file://{self._workspace_path}"
self._init_params["processId"] = None
self._init_params["rootPath"] = workspace_file
self._init_params["rootPath"] = self._workspace_path
self._init_params["rootUri"] = workspace_uri
self._init_params["workspaceFolders"] = [
{
@@ -30,15 +35,22 @@ class LSPClientEvents:
}
]
self._init_params["initializationOptions"] = init_ops
self._init_params["initializationOptions"] = self._init_opts
self.send_request("initialize", self._init_params)
def send_initialized_message(self):
def send_initialized_notification(self):
self.send_notification("initialized")
def send_shutdown_request(self):
self.send_request("shutdown")
def send_exit_notification(self):
self.send_notification("exit")
def _lsp_did_open(self, data: dict):
method = "textDocument/didOpen"
params = didopen_notification["params"]
self.doc_vers[ data["uri"] ] = -1
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
@@ -76,24 +88,24 @@ class LSPClientEvents:
self.send_notification( method, params )
# def _lsp_did_change(self, data: dict):
# method = "textDocument/didChange"
# params = didchange_notification_range["params"]
def _lsp_did_change_range(self, data: dict):
method = "textDocument/didChange"
params = didchange_notification_range["params"]
# params["textDocument"]["uri"] = data["uri"]
# params["textDocument"]["languageId"] = data["language_id"]
# params["textDocument"]["version"] = data["version"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
# contentChanges = params["contentChanges"][0]
# start = contentChanges["range"]["start"]
# end = contentChanges["range"]["end"]
# contentChanges["text"] = data["text"]
# start["line"] = data["line"]
# start["character"] = 0
# end["line"] = data["line"]
# end["character"] = data["column"]
contentChanges = params["contentChanges"][0]
start = contentChanges["range"]["start"]
end = contentChanges["range"]["end"]
contentChanges["text"] = data["text"]
start["line"] = data["line"]
start["character"] = data["column"]
end["line"] = data["end_line"]
end["character"] = data["end_column"]
# self.send_notification( method, params )
self.send_notification( method, params )
def _lsp_definition(self, data: dict):
method = "textDocument/definition"
@@ -107,13 +119,33 @@ class LSPClientEvents:
self.send_request( method, params )
def _lsp_implementation(self, data: dict):
method = "textDocument/implementation"
params = implementation_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_references(self, data: dict):
method = "textDocument/references"
params = references_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_completion(self, data: dict):
method = "textDocument/completion"
params = completion_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]

View File

@@ -11,7 +11,7 @@ from ..dto.code.lsp.lsp_message_structs import \
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from .lsp_client_base import LSPClientBase
from .websocket_client import WebsocketClient
from .websocket import Websocket
@@ -24,26 +24,26 @@ class LSPClientWebsocket(LSPClientBase):
message = f"Content-Length: {message_size}\r\n\r\n{message_str}"
logger.debug(f"Client: {message_str}")
self.ws_client.send(message_str)
self.websocket.send(message_str)
def start_client(self):
self.ws_client = WebsocketClient()
self.ws_client.set_socket(self._socket)
self.ws_client.set_callback(self._monitor_lsp_response)
self.ws_client.start_client()
self.websocket = Websocket()
self.websocket.set_socket(self._socket)
self.websocket.set_callback(self._monitor_lsp_response)
self.websocket.start_client()
return self.ws_client
return self.websocket
def stop_client(self):
if not hasattr(self, "ws_client"): return
self.ws_client.close_client()
if not hasattr(self, "websocket"): return
self.websocket.close_client()
def _monitor_lsp_response(self, data: dict | None):
if not data: return
if not data: return {}
message = get_message_obj(data)
keys = message.keys()
lsp_response = None
lsp_response = data
if "result" in keys:
lsp_response = LSPResponseRequest(**get_message_obj(data))
@@ -51,6 +51,7 @@ class LSPClientWebsocket(LSPClientBase):
if "method" in keys:
lsp_response = LSPResponseNotification(**get_message_obj(data)) if not "id" in keys else LSPIDResponseNotification( **get_message_obj(data) )
if not lsp_response: return
if isinstance(lsp_response, str):
lsp_response = get_message_obj(lsp_response)
GLib.idle_add(self.handle_lsp_response, lsp_response)
GLib.idle_add(self.handle_lsp_response, lsp_response)

View File

@@ -9,7 +9,7 @@ from ..libs import websocket
class WebsocketClient:
class Websocket:
def __init__(self):
self.ws = None
self._socket = None
@@ -59,4 +59,4 @@ class WebsocketClient:
on_error = self.on_error,
on_close = self.on_close)
self.ws.run_forever(reconnect = 0.5)
self.ws.run_forever(reconnect = 0.5)

View File

@@ -4,14 +4,15 @@ from concurrent.futures import ThreadPoolExecutor
# Lib imports
# Application imports
from .mixins.lsp_client_events_mixin import LSPClientEventsMixin
from .config import get_lsp_connect_timout
from .mixins.client_manager_events_mixin import ClientManagerEventsMixin
from .client.lsp_client import LSPClient
class LSPManagerClient(LSPClientEventsMixin):
class ClientManager(ClientManagerEventsMixin):
def __init__(self):
super(LSPManagerClient, self).__init__()
super(ClientManager, self).__init__()
self._cache_refresh_timeout_id: int = None
@@ -22,22 +23,24 @@ class LSPManagerClient(LSPClientEventsMixin):
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {}
lang_id: str,
workspace_path: str,
init_opts: dict[str, str],
address: str = "127.0.0.1",
port: str = "9999"
) -> LSPClient:
if lang_id in self.clients: return None
address = "127.0.0.1"
port = 9999
uri = f"ws://{address}:{port}/{lang_id}"
uri = f"ws://{address}:{port}/{lang_id}?workspace={workspace_path}"
client = LSPClient()
client.set_language(lang_id)
client.set_socket(uri)
client.set_language(lang_id)
client.set_workspace_path(workspace_path)
client.set_init_opts(init_opts)
client.start_client()
if not client.ws_client.wait_for_connection(timeout = 5.0):
if not client.websocket.wait_for_connection(timeout = get_lsp_connect_timout()):
logger.error(f"Failed to connect to LSP server for {lang_id}")
return None

View File

@@ -0,0 +1,87 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import Gtk
from gi.repository import GtkSource
# Application imports
class Commands:
lsp_manager: callable = None
class lsp_manager_toggle:
@staticmethod
def execute(
source_view: GtkSource,
char_str: str,
modkeys_states: tuple
):
logger.debug("Command: LSP Manager Toggle")
if Commands.lsp_manager.ui_manager.is_visible():
Commands.lsp_manager.ui_manager.hide()
else:
Commands.lsp_manager.ui_manager.show()
class lsp_references:
@staticmethod
def execute(
view: GtkSource,
char_str: str,
modkeys_states: tuple
):
logger.debug("Command: LSP References")
file = view.command.exec("get_current_file")
buffer = view.get_buffer()
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
Commands.lsp_manager.client_manager.process_references_definition(
file.ftype, file.fpath, line, column
)
class lsp_implementation:
@staticmethod
def execute(
view: GtkSource,
char_str: str,
modkeys_states: tuple
):
logger.debug("Command: LSP Implements")
file = view.command.exec("get_current_file")
buffer = view.get_buffer()
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
Commands.lsp_manager.client_manager.process_implementation_definition(
file.ftype, file.fpath, line, column
)
class lsp_definition:
@staticmethod
def execute(
view: GtkSource,
char_str: str,
modkeys_states: tuple
):
logger.debug("Command: LSP Definition (Go-To)")
file = view.command.exec("get_current_file")
buffer = view.get_buffer()
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
Commands.lsp_manager.client_manager.process_definition(
file.ftype, file.fpath, line, column
)

View File

@@ -0,0 +1,38 @@
# Python imports
from os import path
import json
# Lib imports
# Application imports
LSP_HOST: str = "127.0.0.1"
LSP_PORT: int = 9999
LSP_CONNECT_TIMOUT: float = 5.0
def get_lsp_host_addr() -> str:
return LSP_HOST
def get_lsp_host_port() -> int:
return LSP_PORT
def get_lsp_connect_timout() -> float:
return LSP_CONNECT_TIMOUT
def get_lsp_init_config() -> dict:
try:
_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()
return json.loads(data)
except Exception as e:
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
return {}

View File

@@ -96,10 +96,10 @@ didchange_notification_range = {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
},
"contentChanges": [
{
"text": "",
"range": {
"start": {
"line": 1,
@@ -108,9 +108,8 @@ didchange_notification_range = {
"end": {
"line": 1,
"character": 1,
},
"rangeLength": 0
}
}
},
}
]
}
@@ -125,19 +124,11 @@ completion_request = {
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "file://",
"languageId": "python",
"version": 1,
"text": ""
"uri": "file://"
},
"position": {
"line": 5,
"character": 12,
"offset": 0
},
"contet": {
"triggerKind": 3,
"triggerCharacter": ""
"character": 12
}
}
}
@@ -159,6 +150,19 @@ definition_request = {
}
}
implementation_request = {
"method": "textDocument/implementation",
"params": {
"textDocument": {
"uri": "file://"
},
"position": {
"line": 5,
"character": 12
}
}
}
references_request = {
"method": "textDocument/references",
"params": {
@@ -179,7 +183,6 @@ references_request = {
}
}
symbols_request = {
"method": "textDocument/documentSymbol",
"params": {
@@ -190,4 +193,15 @@ symbols_request = {
"text": ""
}
}
}
}
shutdown_request = {
"method": "shutdown",
"params": None
}
exit_request = {
"method": "exit",
"params": None
}

View File

@@ -1,6 +1,9 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.controllers.controller_base import ControllerBase
@@ -11,10 +14,11 @@ from .dto.code.events import \
from .dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, LSPResponseRequest, LSPResponseNotification
from .ui_manager import UIManager
from .provider import Provider
from .provider_response_cache import ProviderResponseCache
from .lsp_manager_ui import LSPManagerUI
from .lsp_manager_client import LSPManagerClient
from .client_manager import ClientManager
from .response_handlers.response_registry import ResponseRegistry
@@ -31,16 +35,16 @@ class LSPManager(ControllerBase):
def _init(self):
self.provider: Provider = Provider()
self.response_cache: ProviderResponseCache = ProviderResponseCache()
self.lsp_manager_client: LSPManagerClient = LSPManagerClient()
self.client_manager: ClientManager = ClientManager()
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)
self.ui_manager: LSPManagerUI = UIManager()
self.ui_manager.connect('create-client', self._on_create_client)
self.ui_manager.connect('close-client', self._on_close_client)
def _do_bind_mapping(self):
self.response_cache.set_lsp_manager_client(self.lsp_manager_client)
self.response_cache.set_lsp_manager_client(self.client_manager)
self.provider.response_cache = self.response_cache
self.response_registry.set_event_hub(
self.emit, self.emit_to, self.provider
@@ -49,39 +53,51 @@ class LSPManager(ControllerBase):
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)
self.ui_manager.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)
self.ui_manager.remove_client_listing(event.lang_id)
def _on_create_client(self, ui, lang_id: str, workspace_uri: str) -> bool:
def _on_create_client(self, ui, lang_id: str, workspace_path: str) -> bool:
init_opts = ui.get_init_opts(lang_id)
result = self.create_client(lang_id, workspace_uri, init_opts)
result = self.create_client(
lang_id,
workspace_path,
init_opts,
ui.adddress_entry.get_text(),
f"{ int( ui.adddress_port.get_value() ) }"
)
if result:
ui.toggle_client_buttons(show_close=True)
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)
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)
self.ui_manager.disconnect_by_func(self._on_create_client)
self.ui_manager.disconnect_by_func(self._on_close_client)
def create_client(
self,
lang_id: str = "python",
workspace_uri: str = "",
init_opts: dict = {}
lang_id: str,
workspace_path: str,
init_opts: dict[str, str],
address: str,
port: str
) -> bool:
client = self.lsp_manager_client.create_client(
lang_id, workspace_uri, init_opts
client = self.client_manager.create_client(
lang_id, workspace_path, init_opts, address, port
)
handler = self.response_registry.get_handler(lang_id)
self.lsp_manager_client.active_language_id = lang_id
self.client_manager.active_language_id = lang_id
if not client or not handler:
logger.error(f"LSP Manager: Either 'client' or 'handler' didn't get created...'")
@@ -92,28 +108,46 @@ class LSPManager(ControllerBase):
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)
controller = self.client_manager.get_active_client()
controller.send_shutdown_request()
def _close():
self.client_manager.close_client(lang_id)
self.response_registry.close_handler(lang_id)
return False
GLib.timeout_add(5000, _close)
return True
def server_response(self, lsp_response: LSPResponseTypes):
def server_response(self, lsp_response: LSPResponseTypes | dict):
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'")
if isinstance(lsp_response, dict):
if not self.client_manager.active_language_id in self.client_manager.clients:
logger.debug(f"No LSP client for '{self.client_manager.active_language_id}', skipping 'server_response'")
return
controller = self.lsp_manager_client.get_active_client()
controller = self.client_manager.get_active_client()
if "type" in lsp_response and lsp_response["type"] == "connected":
controller.send_initialize_message()
return
if isinstance(lsp_response, LSPResponseRequest):
if not self.client_manager.active_language_id in self.client_manager.clients:
logger.debug(f"No LSP client for '{self.client_manager.active_language_id}', skipping 'server_response'")
return
controller = self.client_manager.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
self.client_manager.active_language_id, event
)
if not handler: return

View File

@@ -1,227 +0,0 @@
# 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,191 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Code_Event_Types
class ClientManagerEventsMixin:
def _get_controller(self, lang_id, action: str):
controller = self.clients.get(lang_id)
if not controller:
logger.debug(f"No LSP client for '{lang_id}', skipping {action}...")
return controller
def _uri(self, fpath: str) -> str:
return fpath if fpath.startswith("file://") else f"file://{fpath}"
def _text(self, buffer, *, hidden = False):
return buffer.get_text(
*buffer.get_bounds(),
include_hidden_chars=hidden
)
def _version(self, controller, uri, bump = False):
if bump:
controller.doc_vers[uri] = controller.doc_vers.get(uri, -1) + 1
return controller.doc_vers.get(uri, 0)
def _activate(self, lang_id):
self.active_language_id = lang_id
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didOpen")): return
uri = self._uri(f.fpath)
self._activate(f.ftype)
c._lsp_did_open({
"uri": uri,
"language_id": f.ftype,
"text": self._text(f.buffer),
})
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didClose")): return
uri = self._uri(f.fpath)
c.doc_vers.pop(uri, None)
c._lsp_did_close({"uri": uri})
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didSave")): return
uri = self._uri(f.fpath)
self._activate(f.ftype)
c._lsp_did_save({
"uri": uri,
"text": self._text(f.buffer),
})
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didChange")): return
uri = self._uri(f.fpath)
self._activate(f.ftype)
version = self._version(c, uri, bump = True)
c._lsp_did_change({
"uri": uri,
"language_id": f.ftype,
"version": version,
"text": self._text(f.buffer, hidden = True),
})
it = f.buffer.get_iter_at_mark(f.buffer.get_insert())
self._set_cache_refresh_trigger(
f.ftype, f.fpath,
it.get_line(),
it.get_line_offset() + 1
)
def _iter_pos(self, it):
return it.get_line(), it.get_line_offset()
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didChange")): return
uri = self._uri(f.fpath)
self._activate(f.ftype)
start_it = event.location.copy()
end_it = event.location.copy()
if event.length > 1:
start_it.backward_chars(event.length)
sl, sc = self._iter_pos(start_it)
el, ec = self._iter_pos(end_it)
sc -= 0 if event.length > 1 else 1
ec -= 1
version = self._version(c, uri, bump = True)
c._lsp_did_change_range({
"uri": uri,
"language_id": f.ftype,
"version": version,
"text": event.text,
"line": sl,
"column": sc,
"end_line": el,
"end_column": ec,
})
it = event.buffer.get_iter_at_mark(event.buffer.get_insert())
self._set_cache_refresh_trigger(
f.ftype, f.fpath, *self._iter_pos(it)
)
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
f = event.file
if not (c := self._get_controller(f.ftype, "didChange")): return
uri = self._uri(f.fpath)
self._activate(f.ftype)
start_it, end_it = event.start.copy(), event.end.copy()
if start_it.compare(end_it) > 0:
start_it, end_it = end_it, start_it
sl, sc = self._iter_pos(start_it)
el, ec = self._iter_pos(end_it)
version = self._version(c, uri, bump = True)
c._lsp_did_change_range({
"uri": uri,
"language_id": f.ftype,
"version": version,
"text": "",
"line": sl,
"column": sc,
"end_line": el,
"end_column": ec,
})
it = event.buffer.get_iter_at_mark(event.buffer.get_insert())
self._set_cache_refresh_trigger(
f.ftype, f.fpath, *self._iter_pos(it)
)
def _request(self, method, lang_id, fpath, **extra):
if not (c := self._get_controller(lang_id, method)): return
uri = self._uri(fpath)
self._activate(lang_id)
payload = {
"uri": uri,
"language_id": lang_id,
"version": self._version(c, uri),
**extra
}
getattr(c, method)(payload)
def process_definition(self, lang_id, fpath, line, column):
self._request("_lsp_definition", lang_id, fpath, line = line, column = column)
def process_implementation_definition(self, lang_id, fpath, line, column):
self._request("_lsp_implementation", lang_id, fpath, line = line, column = column)
def process_references_definition(self, lang_id, fpath, line, column):
self._request("_lsp_references", lang_id, fpath, line = line, column = column)
def process_completion_request(self, lang_id, fpath, line, column):
self._request("_lsp_completion", lang_id, fpath, line = line, column = column)
def _set_cache_refresh_trigger(self, lang_id, fpath, line, column):
self.process_completion_request(lang_id, fpath, line, column)

View File

@@ -1,144 +0,0 @@
# Python imports
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from libs.event_factory import Code_Event_Types
class LSPClientEventsMixin:
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
lang_id = event.file.ftype
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping didOpen")
return
controller = self.clients[lang_id]
fpath = event.file.fpath
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds())
self.active_language_id = lang_id
controller._lsp_did_open({
"uri": uri,
"language_id": lang_id,
"text": text
})
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
lang_id = event.file.ftype
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping didClose")
return
controller = self.clients[lang_id]
fpath = event.file.fpath
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
controller._lsp_did_close({"uri": uri})
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
lang_id = event.file.ftype
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping didSave")
return
controller = self.clients[lang_id]
fpath = event.file.fpath
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds())
self.active_language_id = lang_id
controller._lsp_did_save({"uri": uri, "text": text})
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
self._clear_delayed_cache_refresh_trigger()
lang_id = event.file.ftype
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping didChange")
return
controller = self.clients[lang_id]
fpath = event.file.fpath
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
buffer = event.file.buffer
text = buffer.get_text(*buffer.get_bounds())
self.active_language_id = lang_id
controller._lsp_did_change({
"uri": uri,
"language_id": lang_id,
"version": 1,
"text": text
})
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
self._set_cache_refresh_trigger(
lang_id, fpath, line, column
)
def process_goto_definition(
self, lang_id: str, fpath: str, line: int, column: int
):
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping goto definition")
return
controller = self.clients[lang_id]
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
self.active_language_id = lang_id
controller._lsp_definition({
"uri": uri,
"language_id": lang_id,
"version": 1,
"line": line,
"column": column
})
def process_completion_request(
self, lang_id: str, fpath: str, line: int, column: int
):
if lang_id not in self.clients:
logger.debug(f"No LSP client for '{lang_id}', skipping completion")
return
controller = self.clients[lang_id]
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
self.active_language_id = lang_id
controller._lsp_completion({
"uri": uri,
"language_id": lang_id,
"version": 1,
"line": line,
"column": column
})
def _clear_delayed_cache_refresh_trigger(self):
if self._cache_refresh_timeout_id:
GLib.source_remove(self._cache_refresh_timeout_id)
def _set_cache_refresh_trigger(
self, lang_id: str, fpath: str, line: int, column: int
):
def trigger_cache_refresh(lang_id, fpath, line, column):
self._cache_refresh_timeout_id = None
self.process_completion_request(
lang_id, fpath, line, column
)
return False
self._cache_refresh_timeout_id = GLib.timeout_add(1500, trigger_cache_refresh, lang_id, fpath, line, column)

View File

@@ -0,0 +1,72 @@
# Python imports
import json
# Lib imports
import gi
from gi.repository import GObject
# Application imports
class UIManagerClientsMixin:
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 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)\
.replace("{user.home}", self._USER_HOME)
self.source_view.get_buffer().set_text(json_str, -1)
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:
self.combo_box.remove(i)
break
self.client_configs.pop(lang_id, None)
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)
def get_init_opts(self, lang_id: str) -> dict:
if not lang_id or lang_id not in self.client_configs: return {}
try:
buffer = self.source_view.get_buffer()
json_str = buffer.get_text(*buffer.get_bounds(), -1)
lang_config = json.loads(json_str)
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON for {lang_id}: {e}")
return {}
return lang_config.get("initialization-options", {})

View File

@@ -0,0 +1,77 @@
# Python imports
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib
from gi.repository import Gtk
# Application imports
class UIManagerEventsMixin:
def _setup_signals(self):
self.connect("show", self._handle_show)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
def _handle_show(self, widget):
GLib.idle_add(self.path_entry.grab_focus)
def _handle_destroy(self, widget):
self.disconnect_by_func(self._handle_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 _on_combo_changed(self, combo: Gtk.ComboBoxText):
lang_id = combo.get_active_text()
self.set_source_view_text( self.path_entry.get_text() )

View File

@@ -0,0 +1,98 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
from gi.repository import Gtk
from gi.repository import GtkSource
# Application imports
from ..config import get_lsp_host_addr, get_lsp_host_port
class UIManagerSetupMixin:
def _setup_styling(self):
self.set_modal(True)
self.set_decorated(False)
self.set_vexpand(True)
self.set_hexpand(True)
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")
self.adddress_entry = Gtk.Entry()
adjustment = Gtk.Adjustment(
value = get_lsp_host_port(),
lower = 1,
upper = 65535,
step_increment = 1,
page_increment = 10,
page_size = 0
)
self.adddress_port = Gtk.SpinButton()
self.adddress_port.set_adjustment(adjustment)
self.adddress_port.set_digits(0) # integers only
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.adddress_entry.set_placeholder_text("Address...")
self.adddress_entry.set_text( get_lsp_host_addr() )
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 = self.adddress_entry, left = 0, top = 1, width = 2, height = 1)
self.main_box.attach(child = self.adddress_port, left = 2, top = 1, width = 2, height = 1)
self.main_box.attach(child = bttn_box, left = 4, top = 1, width = 3, 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 set_source_view(self, scrolled_win, source_view):
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)
self.main_box.attach(scrolled_win, 0, 2, 7, 1)

View File

@@ -12,6 +12,7 @@ from libs.dto.states import SourceViewStates
from plugins.plugin_types import PluginCode
from .dto.code import events as lsp_events
from .commands import Commands
from .lsp_manager import LSPManager
@@ -35,23 +36,11 @@ class Plugin(PluginCode):
window = self.request_ui_element("main-window")
lsp_manager.lsp_manager_ui.map_parent_resize_event(window)
lsp_manager.ui_manager.map_parent_resize_event(window)
event = Event_Factory.create_event("register_command",
command_name = "LSP Manager",
command = Handler,
binding_mode = "released",
binding = ["<Shift><Control>l", "<Control>g", "<Control>i"]
)
self.emit_to("source_views", event)
self._manage_signals("register_command")
event = Event_Factory.create_event(
"register_provider",
provider_name = "LSP Completer",
provider = lsp_manager.provider,
language_ids = []
)
self.emit_to("completion", event)
self._manage_provider("register_provider")
event = Event_Factory.create_event(
"create_source_view",
@@ -59,8 +48,8 @@ class Plugin(PluginCode):
)
self.emit_to("source_views", event)
source_view = event.response
lsp_manager.lsp_manager_ui.set_source_view(source_view)
scrolled_win, source_view = event.response
lsp_manager.ui_manager.set_source_view(scrolled_win, source_view)
def unload(self):
Event_Factory.unregister_events( lsp_events.__dict__.items() )
@@ -69,56 +58,61 @@ class Plugin(PluginCode):
window = self.request_ui_element("main-window")
lsp_manager.lsp_manager_ui.unmap_parent_resize_event(window)
lsp_manager.ui_manager.unmap_parent_resize_event(window)
event = Event_Factory.create_event("unregister_command",
command_name = "LSP Manager",
command = Handler,
self._manage_signals("unregister_command")
self._manage_provider("unregister_provider")
lsp_manager.handle_destroy()
def _manage_signals(self, action: str):
_commands = Commands
_commands.lsp_manager = lsp_manager
event = Event_Factory.create_event(action,
command_name = "lsp_manager_toggle",
command = _commands.lsp_manager_toggle,
binding_mode = "released",
binding = ["<Shift><Control>l", "<Control>g", "<Control>i"]
binding = "<Shift><Control>l"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(
"unregister_provider",
provider_name = "LSP Completer"
event = Event_Factory.create_event(action,
command_name = "lsp_references",
command = _commands.lsp_references,
binding_mode = "released",
binding = "<Control>i"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "lsp_implementation",
command = _commands.lsp_implementation,
binding_mode = "released",
binding = "<Shift><Control>i"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(action,
command_name = "lsp_definition",
command = _commands.lsp_definition,
binding_mode = "released",
binding = "<Control>g"
)
self.emit_to("source_views", event)
def _manage_provider(self, action: str):
event = Event_Factory.create_event(
action,
provider_name = "LSP Completer",
provider = lsp_manager.provider,
language_ids = []
)
self.emit_to("completion", event)
lsp_manager.handle_destroy()
def run(self):
...
def generate_plugin_element(self):
...
class Handler:
@staticmethod
def execute(
view: any,
*args,
**kwargs
):
logger.debug("Command: LSP Manager")
char_str = args[0]
if char_str in ["g", "i"]:
file = view.command.exec("get_current_file")
buffer = view.get_buffer()
iter = buffer.get_iter_at_mark( buffer.get_insert() )
line = iter.get_line()
column = iter.get_line_offset()
if char_str == "g":
lsp_manager.lsp_manager_client.process_goto_definition(
file.ftype, file.fpath, line, column
)
return
if char_str == "i":
return
lsp_manager.lsp_manager_ui.hide() if lsp_manager.lsp_manager_ui.is_visible() else lsp_manager.lsp_manager_ui.show()

View File

@@ -32,20 +32,7 @@ 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 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()):
return False
buffer = iter.get_buffer()
if buffer.get_context_classes_at_iter(iter) != ['no-spell-check']:
@@ -68,6 +55,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
# return GtkSource.CompletionActivation.NONE
return GtkSource.CompletionActivation.USER_REQUESTED
# return GtkSource.CompletionActivation.INTERACTIVE
# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE
def do_populate(self, context):
results = self.response_cache.filter_with_context(context)

View File

@@ -33,9 +33,13 @@ class ProviderResponseCache(ProviderResponseCacheBase):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_save(event)
def process_file_change(self, event):
def process_file_text_inserted(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_change(event)
self.lsp_manager_client.process_file_text_inserted(event)
def process_file_delete_range(self, event):
if self.lsp_manager_client:
self.lsp_manager_client.process_file_delete_range(event)
def filter(self, word: str) -> list[dict]:
return []

View File

@@ -17,10 +17,18 @@ class DefaultHandler(BaseHandler):
def handle(self, method: str, response, controller):
match method:
case "initialize":
controller.send_initialized_notification()
case "shutdown":
controller.send_exit_notification()
case "textDocument/completion":
self._handle_completion(response)
case "textDocument/definition":
self._handle_definition(response, controller)
case "textDocument/references":
...
case "textDocument/implementation":
...
case "textDocument/publishDiagnostics":
self._handle_diagnostics(response)
@@ -130,5 +138,5 @@ class DefaultHandler(BaseHandler):
view = view,
provider = self.context._provider
)
self.emit_to("completion", event)
self.emit_to("completion", event)

View File

@@ -40,7 +40,7 @@ class ResponseRegistry:
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)

View File

@@ -0,0 +1,44 @@
# Python imports
from os import path
# Lib imports
import gi
from gi.repository import GObject
from gi.repository import Gtk
# Application imports
from .mixins.ui_manager_setup_mixin import UIManagerSetupMixin
from .mixins.ui_manager_events_mixin import UIManagerEventsMixin
from .mixins.ui_manager_clients_mixin import UIManagerClientsMixin
class UIManager(
Gtk.Dialog,
UIManagerSetupMixin,
UIManagerEventsMixin,
UIManagerClientsMixin
):
__gsignals__ = {
'create-client': (GObject.SignalFlags.RUN_LAST, None, (str, str)),
'close-client': (GObject.SignalFlags.RUN_LAST, None, (str,)),
}
def __init__(self):
super(UIManager, self).__init__()
self._USER_HOME = path.expanduser("~")
self.client_configs: dict[str, str] = {}
self.source_view = None
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
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)

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

@@ -47,9 +47,16 @@
},
"jedi_completion": {
"enabled": true,
"fuzzy": true,
"include_params": false,
"include_class_objects": true,
"include_function_objects": true,
"fuzzy": false
"include_function_objects": true
},
"jedi_signature_help": {
"enabled": true
},
"jedi_references": {
"enabled": true
},
"jedi": {
"root_dir": "file://{workspace.folder}",
@@ -60,41 +67,4 @@
}
}
}
},
"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,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -0,0 +1,86 @@
# Python imports
# Lib imports
# Application imports
FOLD_NODES = {
"python": {
"function_definition",
"class_definition",
"if_statement",
"for_statement",
"while_statement",
"with_statement",
"try_statement",
},
"python3": {
"function_definition",
"class_definition",
"if_statement",
"for_statement",
"while_statement",
"with_statement",
"try_statement",
},
"javascript": {
"function_declaration",
"class_declaration",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
"try_statement",
},
"html": {
"element",
"attribute",
},
"css": {
"rule_set",
"selector",
"declaration",
},
"json": {
"object",
"array",
},
"java": {
"class_declaration",
"method_declaration",
"constructor_declaration",
"if_statement",
"for_statement",
"while_statement",
"switch_expression",
"block",
},
"c": {
"function_definition",
"struct_definition",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
},
"cpp": {
"function_definition",
"class_definition",
"struct_definition",
"namespace_definition",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
},
"go": {
"function_declaration",
"type_declaration",
"if_statement",
"for_statement",
"select_statement",
"switch_statement",
},
}

View File

@@ -0,0 +1,24 @@
# Python imports
# Lib imports
# Application imports
def collapse_range(view, fold):
buffer = view.get_buffer()
start = buffer.get_iter_at_line(fold["start_line"] + 1)
end = buffer.get_iter_at_line(fold["end_line"] + 1)
buffer.apply_tag_by_name("invisible", start, end)
def expand_range(view, fold):
buffer = view.get_buffer()
start = buffer.get_iter_at_line(fold["start_line"] + 1)
end = buffer.get_iter_at_line(fold["end_line"] + 1)
buffer.remove_tag_by_name("invisible", start, end)

View File

@@ -0,0 +1,32 @@
# Python imports
# Lib imports
# Application imports
from .fold_types import FOLD_NODES
def visit_node(fold_types, node, ranges):
if node.type in fold_types:
start_line = node.start_point[0]
end_line = node.end_point[0]
if end_line > start_line:
ranges.append({
"start_line": start_line,
"end_line": end_line,
"id": (start_line, end_line, node.type),
})
for child in node.children:
visit_node(fold_types, child, ranges)
def get_folding_ranges(lang_name: str, ast):
root = ast.root_node
fold_types = FOLD_NODES.get(lang_name, set())
ranges = []
visit_node(fold_types, root, ranges)
return ranges

View File

@@ -0,0 +1,90 @@
# Python imports
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gtk
from gi.repository import GtkSource
# Application imports
from .folding_actions import collapse_range, expand_range
def handle_collapse(view, fold):
collapse_range(view, fold)
def handle_expand(view, fold):
expand_range(view, fold)
def handle_block_toggle(collapsed, view, fold):
if not collapsed:
handle_collapse(view, fold)
else:
handle_expand(view, fold)
def is_fold_hidden(buffer, fold):
iter_ = buffer.get_iter_at_line(fold["start_line"] + 1)
tags = iter_.get_tags()
return any(tag.get_property("invisible") for tag in tags)
def on_query_data(renderer, start_iter, end_iter, state, view):
line = start_iter.get_line()
if not line in view.fold_start_set:
renderer.set_text("", -1)
return
fold = next(
(f for f in view.fold_starts if f["start_line"] == line), None
)
collapsed = fold and is_fold_hidden(view.get_buffer(), fold)
renderer.set_text("" if collapsed else "", -1)
def on_click(view, event, renderer):
if not event.button == 1: return False
window = view.get_window(Gtk.TextWindowType.LEFT)
if not event.window == window: return False
x, y = view.window_to_buffer_coords(
Gtk.TextWindowType.LEFT, int(event.x), int(event.y)
)
_, iter_ = view.get_iter_at_location(x, y)
line = iter_.get_line()
if line not in view.fold_start_set: return False
for fold in view.fold_starts:
if not fold["start_line"] == line: continue
collapsed = is_fold_hidden(view.get_buffer(), fold)
handle_block_toggle(collapsed, view, fold)
renderer.queue_draw()
return True
return False
def setup_gutter(view):
gutter = view.get_gutter(Gtk.TextWindowType.LEFT)
buffer = view.get_buffer()
view.fold_starts = []
view.fold_start_set = set()
renderer = GtkSource.GutterRendererText()
renderer.set_size(12)
renderer.set_padding(2, -1)
renderer.query_data_id = renderer.connect("query-data", on_query_data, view)
view.collapse_click_id = view.connect("button-press-event", on_click, renderer)
gutter.insert(renderer, 0)
view.fold_renderer = renderer

View File

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

View File

@@ -0,0 +1,94 @@
# Python imports
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .fold_types import FOLD_NODES
from .folding_actions import collapse_range
from .folding_engine import get_folding_ranges
from .gutter_renderer import setup_gutter
class Plugin(PluginCode):
def _controller_message(self, event):
if isinstance(event, Code_Event_Types.FocusedViewEvent):
self.view = event.view
event = Event_Factory.create_event(
"get_file", buffer = self.view.get_buffer()
)
self.emit_to("files", event)
file = event.response
if not file: return
if file.ftype not in FOLD_NODES:
self.view.fold_start_set = {}
return
if not hasattr(file, "ast"):
self.view.fold_start_set = {}
return
buffer = file.buffer
if not buffer.get_tag_table().lookup("invisible"):
tag = buffer.create_tag("invisible")
tag.set_property("invisible", True)
self.update_gutter(file, self.view)
elif isinstance(event, Code_Event_Types.TextChangedEvent):
if event.file.ftype not in FOLD_NODES: return
if not hasattr(event.file, "ast"): return
self.schedule_update(event.file, self.view)
def load(self):
event = Event_Factory.create_event("get_source_views")
self.emit_to("source_views", event)
for view in event.response:
setup_gutter(view)
def unload(self):
event = Event_Factory.create_event("get_source_views")
self.emit_to("source_views", event)
for view in event.response:
view.fold_renderer.disconnect(view.fold_renderer.query_data_id)
view.disconnect(view.collapse_click_id)
gutter = view.get_gutter(Gtk.TextWindowType.LEFT)
gutter.remove(view.fold_renderer)
def run(self):
...
def schedule_update(self, file, view, delay=250):
if hasattr(view, "fold_update_source") and view.fold_update_source:
GLib.source_remove(view.fold_update_source)
def callback():
self.update_gutter(file, view)
if hasattr(view, "fold_renderer"):
view.fold_renderer.queue_draw()
view.fold_update_source = None
return False
view.fold_update_source = GLib.timeout_add(delay, callback)
def update_gutter(self, file, view):
view.fold_starts = get_folding_ranges(file.ftype, file.ast)
view.fold_start_set = {
fold["start_line"] for fold in view.fold_starts
}

View File

@@ -30,6 +30,7 @@ class Plugin(PluginCode):
event = Event_Factory.create_event("get_active_view")
self.emit_to("source_views", event)
if not event.response: return
code_minimap.set_smini_view(event.response)
def unload(self):

View File

@@ -99,7 +99,7 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin):
def _tggle_preview_updates(self, widget):
self.is_preview_paused = not self.is_preview_paused
def _handle_destroy(self):
def _handle_destroy(self, widget):
self.disconnect_by_func(self._handle_hide)
self.disconnect_by_func(self._handle_show)
self.disconnect_by_func(self._handle_destroy)

View File

@@ -41,18 +41,14 @@ class Plugin(PluginCode):
def load(self):
separator_right = self.request_ui_element("separator-right")
markdown_preview.set_relative_to(separator_right)
event = Event_Factory.create_event("register_command",
command_name = "tggle_markdown_preview",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>m"
)
self.emit_to("source_views", event)
self._manage_signals("register_command")
def unload(self):
event = Event_Factory.create_event("unregister_command",
self._manage_signals("unregister_command")
markdown_preview.destroy()
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "tggle_markdown_preview",
command = Handler,
binding_mode = "released",
@@ -61,8 +57,6 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
markdown_preview.destroy()
def run(self):
...

View File

@@ -52,8 +52,8 @@ class Plugin(PluginCode):
)
self.emit_to("source_views", event)
source_view = event.response
telescope.set_source_view(source_view)
scrolled_win, source_view = event.response
telescope.set_source_view(scrolled_win, source_view)
event = Event_Factory.create_event(
"register_completer",

View File

@@ -99,13 +99,9 @@ class Telescope(Gtk.Dialog):
def unmap_parent_resize_event(self, parent):
parent.disconnect(self.size_allocate_id)
def set_source_view(self, source_view):
scrolled_win = Gtk.ScrolledWindow()
def set_source_view(self, scrolled_win, source_view):
self.source_view = source_view
scrolled_win.add(self.source_view)
self.main_box.pack_end(scrolled_win, True, True, 0)
scrolled_win.show_all()
def _handle_destroy(self, widget):

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .terminals_view import TerminalsView
terminals_view = TerminalsView()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
terminals_view.emit_to = self.emit_to
footer = self.request_ui_element("footer-container")
footer.add( terminals_view )
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
terminals_view.destroy()
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "terminals",
command = Handler,
binding_mode = "released",
binding = "<Control>."
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
*args,
**kwargs
):
logger.debug("Command: Terminal")
terminals_view.set_code_view(view)
terminals_view.hide() if terminals_view.is_visible() else terminals_view.show()

View File

@@ -0,0 +1,206 @@
# Python imports
import os
import shlex
# Lib imports
import gi
gi.require_version('Gio', '2.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Pango
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .vte_widget import VteWidget
class TerminalsView(Gtk.Notebook):
def __init__(self):
super(TerminalsView, self).__init__()
self.MARKERS: list = ["src", ".git", ".gitignore", "README.md"]
self.code_view = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
self.hide()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("terminals-view")
self.set_scrollable(True)
def _setup_signals(self):
self.connect("show", self._handle_show)
self.connect("hide", self._handle_hide)
self.connect("destroy", self._handle_destroy)
def _load_widgets(self):
hbox = Gtk.Box()
self.add_bttn = Gtk.Button(label = "")
self.hide_bttn = Gtk.Button(label = "")
self.add_bttn.connect("clicked", self._create_terminal)
self.hide_bttn.connect("clicked", self._hide_view)
hbox.add(self.add_bttn)
hbox.add(self.hide_bttn)
self.set_action_widget(hbox, Gtk.PackType.END)
self.create_terminal()
hbox.show_all()
def _generate_terminal_parts(self):
label = Gtk.Label(label = "...")
vte_widget = VteWidget()
vte_widget.hide_view = self.hide
vte_widget.go_to_project_or_home = self.go_to_project_or_home
vte_widget.create_terminal = self.create_terminal
vte_widget.close_terminal = self.close_terminal
vte_widget.prev_terminal = self.prev_terminal
vte_widget.next_terminal = self.next_terminal
label.set_text( vte_widget.get_home_path() )
label.set_tooltip_text( vte_widget.get_home_path() )
label.set_ellipsize(Pango.EllipsizeMode.START)
label.set_single_line_mode(True)
label.set_max_width_chars(32)
label.set_size_request(240, -1)
vte_widget.bind_label(label)
return label, vte_widget
def _handle_show(self, widget):
i = widget.get_current_page()
term = widget.get_nth_page(i)
GLib.idle_add(term.grab_focus)
def _handle_hide(self, widget):
if not self.code_view: return
GLib.idle_add(self.code_view.grab_focus)
def _hide_view(self, widget):
self.hide()
def _handle_destroy(self, widget):
widget.disconnect_by_func(widget._handle_show)
widget.disconnect_by_func(widget._handle_hide)
widget.disconnect_by_func(widget._handle_destroy)
self.add_bttn.disconnect_by_func(self._create_terminal)
self.hide_bttn.disconnect_by_func(self._hide_view)
def _create_terminal(self, widget):
self.create_terminal()
def has_marker(self, gfile):
try:
enumerator = gfile.enumerate_children(
"standard::name,standard::type",
Gio.FileQueryInfoFlags.NONE,
None
)
while True:
info = enumerator.next_file(None)
if info is None:
break
if info.get_name() in self.MARKERS:
enumerator.close(None)
return True
enumerator.close(None)
except Exception:
pass
return False
def find_project_path_or_home(self, current: Gio.File):
if not current: return
home = Gio.File.new_for_path( os.path.expanduser("~") )
while True:
if self.has_marker(current):
return current.get_path()
if current.equal(home):
return current.get_path()
parent = current.get_parent()
if parent is None:
return current.get_path()
current = parent
def set_code_view(self, widget):
self.code_view = widget
def go_to_project_or_home(self):
event = Event_Factory.create_event("get_file",
buffer = self.code_view.get_buffer()
)
self.emit_to("files", event)
if event.response.ftype == "buffer": return
gfile = event.response.get_location().get_parent()
fpath = self.find_project_path_or_home(gfile)
i = self.get_current_page()
widget = self.get_nth_page(i)
widget.run_command(f"cd {shlex.quote(fpath)} && clear\n")
def create_terminal(self):
label, vte_widget = self._generate_terminal_parts()
index = self.append_page(vte_widget, label)
self.set_tab_detachable(vte_widget, True)
self.set_tab_reorderable(vte_widget, True)
self.set_current_page(index)
GLib.idle_add(vte_widget.grab_focus)
self.show_all()
def close_terminal(self):
size = self.get_n_pages()
if size == 1: return
i = self.get_current_page()
widget = self.get_nth_page(i)
self.remove_page(i)
widget.destroy()
def prev_terminal(self):
i = self.get_current_page() - 1
size = self.get_n_pages()
if i < 0:
self.set_current_page(size - 1)
return
self.prev_page()
def next_terminal(self):
i = self.get_current_page() + 1
size = self.get_n_pages()
if i == size:
self.set_current_page(0)
return
self.next_page()

View File

@@ -0,0 +1,180 @@
# Python imports
import os
from os import path
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Vte
# Application imports
class VteWidgetException(Exception):
...
class VteWidget(Vte.Terminal):
"""
https://stackoverflow.com/questions/60454326/how-to-implement-a-linux-terminal-in-a-pygtk-app-like-vscode-and-pycharm-has
"""
def __init__(self):
super(VteWidget, self).__init__()
self._USER_HOME: str = path.expanduser('~')
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._do_session_spawn()
self.show()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("vte-widget")
self.set_clear_background(False)
self.set_hexpand(True)
self.set_enable_sixel(True)
self.set_cursor_shape( Vte.CursorShape.IBEAM )
self.set_audible_bell(False)
self.set_scroll_on_output(True)
def _setup_signals(self):
self.connect("commit", self._handle_commit)
self.connect("current-directory-uri-changed", self._handle_path_change)
self.connect("selection-changed", self._handle_selection)
self.connect("button-press-event", self._on_button_press)
self.connect("key-press-event", self._on_key_press)
self.connect("key-release-event", self._on_key_release)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
def _load_widgets(self):
...
def _do_session_spawn(self):
env_dict = os.environ.copy()
existing_pc = env_dict.get("PROMPT_COMMAND", "")
# Note: Needed for 'current-directory-uri-changed' to work.
# Make sure user .bashrc doesn't affect it...
osc7 = 'printf "\\033]7;file://%s%s\\007" "$PWD"'
env_dict.update({
"LC_ALL": "C",
"TERM": "xterm-256color",
"HISTFILE": "/dev/null",
"HISTSIZE": "0",
"HISTFILESIZE": "0",
"PS1": "\\h@\\u \\W -->: ",
"PROMPT_COMMAND": f"{osc7};{existing_pc}" if existing_pc else osc7,
})
env = [f"{k}={v}" for k, v in env_dict.items()]
self.spawn_async(
Vte.PtyFlags.DEFAULT,
self._USER_HOME,
["/bin/bash"],
env,
GLib.SpawnFlags.DEFAULT,
None, None, -1, None, None,
)
self.set_scrollback_lines(15000)
def _handle_destroy(self, terminal):
logger.debug("Destroying terminal...")
terminal.disconnect_by_func(terminal._handle_commit)
terminal.disconnect_by_func(terminal._handle_path_change)
terminal.disconnect_by_func(terminal._handle_selection)
terminal.disconnect_by_func(terminal._on_button_press)
terminal.disconnect_by_func(terminal._on_key_press)
terminal.disconnect_by_func(terminal._on_key_release)
terminal.disconnect_by_func(terminal._handle_destroy)
def _handle_commit(self, terminal, text, size):
...
def _handle_selection(self, *args):
if self.get_has_selection():
self.copy_primary()
def _on_button_press(self, widget, event):
if event.button == 2: # middle click
self.paste_clipboard()
return True
def _on_key_press(self, widget, event):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK
shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK
if event.keyval == Gdk.KEY_Home:
self.go_to_project_or_home()
return True
if ctrl_pressed:
if shift_pressed:
if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]:
if event.keyval == Gdk.KEY_C:
self.copy_clipboard()
elif event.keyval == Gdk.KEY_V:
self.paste_clipboard()
return True
if event.keyval in [
Gdk.KEY_period, Gdk.KEY_t, Gdk.KEY_w, Gdk.KEY_Up, Gdk.KEY_Down
]:
if event.keyval == Gdk.KEY_period:
if hasattr(self, "hide_view"):
GLib.timeout_add(200, self.hide_view)
elif event.keyval == Gdk.KEY_t:
if hasattr(self, "create_terminal"):
self.create_terminal()
elif event.keyval == Gdk.KEY_w:
if hasattr(self, "close_terminal"):
self.close_terminal()
elif event.keyval == Gdk.KEY_Up:
if hasattr(self, "prev_terminal"):
self.prev_terminal()
elif event.keyval == Gdk.KEY_Down:
if hasattr(self, "next_terminal"):
self.next_terminal()
return True
return False
def _on_key_release(self, widget, event):
...
def _handle_path_change(self, terminal):
if not hasattr(self, "label"): return
uri = terminal.get_current_directory_uri().replace("file://", "")
terminal.label.set_text(uri)
terminal.label.set_tooltip_text(uri)
def get_home_path(self):
return self._USER_HOME
def bind_label(self, label: Gtk.Label):
self.label = label
def run_command(self, cmd: str):
self.feed_child_binary(bytes(cmd, 'utf8'))

View File

@@ -6,9 +6,10 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from core.widgets.save_file_dialog import SaveFileDialog
from core.widgets.controls.open_files_button import OpenFilesButton
from .code.code_container import CodeContainer
from ..widgets.save_file_dialog import SaveFileDialog
from ..widgets.controls.open_files_button import OpenFilesButton

View File

@@ -6,10 +6,6 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ...widgets.code.code_base import CodeBase
from ...widgets.separator_widget import Separator
from .editors_container import EditorsContainer
@@ -27,6 +23,9 @@ class CodeContainer(Gtk.Box):
def _setup_styling(self):
self.ctx = self.get_style_context()
self.ctx.add_class("code-container")
self.set_orientation(Gtk.Orientation.VERTICAL)
def _setup_signals(self):
@@ -37,18 +36,4 @@ class CodeContainer(Gtk.Box):
def _load_widgets(self):
widget_registery.expose_object("code-container", self)
code_base = CodeBase()
self.add( self._create_editor_widget(code_base) )
def _create_editor_widget(self, code_base: CodeBase):
editors_container = Gtk.Box()
widget_registery.expose_object("editors-container", editors_container)
editors_container.add( Separator("separator_left") )
editors_container.add( EditorsContainer(code_base) )
editors_container.add( Separator("separator_right") )
return editors_container
self.add( EditorsContainer() )

View File

@@ -4,18 +4,17 @@
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#from gi.repository import GLib
# Application imports
from core.widgets.code.code_base import CodeBase
from core.widgets.separator_widget import Separator
class EditorsContainer(Gtk.Paned):
def __init__(self, code_base: any):
class EditorsContainer(Gtk.Box):
def __init__(self):
super(EditorsContainer, self).__init__()
self.code_base = code_base
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
@@ -24,11 +23,11 @@ class EditorsContainer(Gtk.Paned):
def _setup_styling(self):
self.ctx = self.get_style_context()
self.ctx.add_class("paned-editors-container")
self.ctx.add_class("editors-container")
self.set_hexpand(True)
self.set_vexpand(True)
self.set_wide_handle(True)
self.set_size_request(320, -1)
def _setup_signals(self):
self.connect("map", self._init_map)
@@ -37,29 +36,18 @@ class EditorsContainer(Gtk.Paned):
...
def _load_widgets(self):
self.scrolled_win1, \
self.scrolled_win2 = self._create_views()
box = Gtk.Box()
widget_registery.expose_object("editors-container", self)
self.code_base = CodeBase()
scrolled_win, \
source_view = self.code_base.create_source_view()
self.pack1( self.scrolled_win1, True, True )
self.pack2( self.scrolled_win2, True, True )
def _create_views(self):
scrolled_win1 = Gtk.ScrolledWindow()
scrolled_win2 = Gtk.ScrolledWindow()
source_view1 = self.code_base.create_source_view()
source_view2 = self.code_base.create_source_view()
source_view1.sibling_right = source_view2
source_view2.sibling_left = source_view1
scrolled_win1.add( source_view1 )
scrolled_win2.add( source_view2 )
return scrolled_win1, scrolled_win2
box.add( scrolled_win )
self.add( Separator("separator_left") )
self.add( box )
self.add( Separator("separator_right") )
def _init_map(self, view):
self.disconnect_by_func( self._init_map )
self.code_base.first_map_load()
self.code_base = None
del self.code_base

View File

@@ -6,8 +6,8 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ..widgets.separator_widget import Separator
from ..widgets.vte_widget import VteWidget
from core.widgets.separator_widget import Separator
from core.widgets.vte_widget import VteWidget

View File

@@ -6,7 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ..widgets.separator_widget import Separator
from core.widgets.separator_widget import Separator

View File

@@ -6,7 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ..widgets.separator_widget import Separator
from core.widgets.separator_widget import Separator

View File

@@ -6,7 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ..widgets.separator_widget import Separator
from core.widgets.separator_widget import Separator

View File

@@ -18,16 +18,12 @@ class BaseControllerMixin:
files = []
for arg in unknownargs + [args.new_tab,]:
if os.path.isdir( arg.replace("file://", "") ):
files.append( f"DIR|{arg.replace('file://', '')}" )
continue
if os.path.isfile(arg):
files.append(f"{arg}")
# NOTE: If passing line number with file split against :
if os.path.isfile( arg.replace("file://", "").split(":")[0] ):
files.append( f"FILE|{arg.replace('file://', '')}" )
continue
logger.info(f"Not a File: {arg}")
if os.path.isdir(arg):
message = f"DIR|{arg}"
ipc_server.send_ipc_message(message)
if not files: return

View File

@@ -34,7 +34,6 @@ class CodeBase:
completion_controller = CompletionController()
source_views_controller = SourceViewsController()
# self.controller_manager.register_controller("base", self)
self.controller_manager.register_controller("files", files_controller)
self.controller_manager.register_controller("commands", commands_controller)
self.controller_manager.register_controller("completion", completion_controller)
@@ -43,12 +42,13 @@ class CodeBase:
self.controller_manager.register_controller("widgets", widget_registery)
def create_source_view(self):
source_view = self.controller_manager["source_views"].create_source_view()
scrolled_win, \
source_view = self.controller_manager["source_views"].create_source_view()
self.controller_manager["completion"].register_completer(
source_view.get_completion()
)
return source_view
return scrolled_win, source_view
def first_map_load(self):
self.controller_manager["source_views"].first_map_load()

View File

@@ -2,4 +2,4 @@
Code Command System Package
"""
from .command_system import CommandSystem
from .source_view_command_system import SourceViewCommandSystem

View File

@@ -8,7 +8,10 @@ from gi.repository import GtkSource
def set_language_and_style(view, file):
language = view.language_manager.guess_language(file.fname, None)
language = None
if not file.fname == "buffer":
language = view.language_manager.guess_language(file.fname, None)
file.buffer.set_language(language)
file.buffer.set_style_scheme(view.syntax_theme)

View File

@@ -1,120 +0,0 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from ..mixins.command_system_mixin import CommandSystemMixin
from ..source_view import SourceView
from . import commands
class CommandSystem(CommandSystemMixin):
def __init__(self):
super(CommandSystem, self).__init__()
self.data: list = ()
def set_data(self, *args, **kwargs):
self.data = (args, kwargs)
def exec(self, command: str) -> any:
if not hasattr(commands, command): return
method = getattr(commands, command)
args, kwargs = self.data
return method.execute(*args, **kwargs)
def exec_with_args(self, command: str, *args, **kwargs) -> any:
if not hasattr(commands, command): return
method = getattr(commands, command)
return method.execute(*args, **kwargs)
def add_command(self, command_name: str, command: callable):
setattr(commands, command_name, command)
def remove_command(self, command_name: str, command: callable):
if hasattr(commands, command_name):
delattr(commands, command_name)
def emit(self, event: Code_Event_Types.CodeEvent):
""" Monkey patch 'emit' from command controller... """
...
def emit_to(self, controller: str, event: Code_Event_Types.CodeEvent):
""" Monkey patch 'emit_to' from command controller... """
...
# def filter_out_loaded_files(self, uris: list[str]):
# event = Event_Factory.create_event(
# "filter_out_loaded_files",
# uris = uris
# )
#
# self.emit_to("files", event)
#
# return event.response
#
# def set_info_labels(self, data: tuple[str]):
# event = Event_Factory.create_event(
# "set_info_labels",
# info = data
# )
#
# self.emit_to("plugins", event)
#
# def get_file(self, view: SourceView):
# event = Event_Factory.create_event(
# "get_file",
# view = view,
# buffer = view.get_buffer()
# )
#
# self.emit_to("files", event)
#
# return event.response
#
# def get_swap_file(self, view: SourceView):
# event = Event_Factory.create_event(
# "get_swap_file",
# view = view,
# buffer = view.get_buffer()
# )
#
# self.emit_to("files", event)
#
# return event.response
#
# def new_file(self, view: SourceView):
# event = Event_Factory.create_event("add_new_file", view = view)
#
# self.emit_to("files", event)
#
# return event.response
#
# def remove_file(self, view: SourceView):
# event = Event_Factory.create_event(
# "remove_file",
# view = view,
# buffer = view.get_buffer()
# )
#
# self.emit_to("files", event)
#
# return event.response
#
# def request_completion(self, view: SourceView):
# event = Event_Factory.create_event(
# "request_completion",
# view = view,
# buffer = view.get_buffer()
# )
#
# self.emit_to("completion", event)

View File

@@ -11,45 +11,45 @@ from gi.repository import GtkSource
def execute(
view: GtkSource.View,
*args,
**kwargs
):
def execute(view: GtkSource.View, *args, **kwargs):
logger.debug("Command: Duplicate Line")
buffer = view.get_buffer()
if not buffer.get_has_selection():
had_selection = False
itr = buffer.get_iter_at_mark( buffer.get_insert() )
start_itr = itr.copy()
end_itr = itr.copy()
start_line = itr.get_line() + 1
start_char = itr.get_line_offset()
if buffer.get_has_selection():
start_itr, \
end_itr = buffer.get_selection_bounds()
start_line = start_itr.get_line()
end_line = end_itr.get_line()
scol = start_itr.get_line_offset()
ecol = end_itr.get_line_offset()
else:
had_selection = True
start_itr, end_itr = buffer.get_selection_bounds()
sline = start_itr.get_line()
eline = end_itr.get_line()
start_line = eline + 1
start_char = start_itr.get_line_offset()
end_char = end_itr.get_line_offset()
range_line_size = eline - sline
itr = buffer.get_iter_at_mark(buffer.get_insert())
start_line = end_line = itr.get_line()
col = itr.get_line_offset()
start_itr.backward_visible_line()
start_itr.forward_line()
end_itr.forward_line()
end_itr.backward_char()
start_itr = buffer.get_iter_at_line(start_line)
end_itr = buffer.get_iter_at_line(end_line)
line_str = buffer.get_slice(start_itr, end_itr, True)
end_itr.forward_char()
buffer.insert(end_itr, f"{line_str}\n", -1)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
if not had_selection:
new_itr = buffer.get_iter_at_line_offset(start_line, start_char)
buffer.place_cursor(new_itr)
if not end_itr.is_end():
end_itr.forward_char()
text = buffer.get_text(start_itr, end_itr, True)
insert_itr = buffer.get_iter_at_line(end_line)
insert_itr.forward_to_line_end()
if not insert_itr.is_end():
insert_itr.forward_char()
buffer.insert(insert_itr, text)
if buffer.get_has_selection():
new_start = buffer.get_iter_at_line_offset(end_line + 1, scol)
new_end = buffer.get_iter_at_line_offset(end_line + 1 + (end_line - start_line), ecol)
buffer.select_range(new_start, new_end)
else:
new_itr = buffer.get_iter_at_line_offset(start_line, start_char)
new_end_itr = buffer.get_iter_at_line_offset((start_line + range_line_size), end_char)
buffer.select_range(new_itr, new_end_itr)
new_start = buffer.get_iter_at_line_offset(end_line + 1, col)
buffer.place_cursor(new_start)

View File

@@ -20,9 +20,14 @@ def execute(
ctx = view.get_parent().get_style_context()
ctx.add_class("source-view-focused")
if view.sibling_right:
ctx = view.sibling_right.get_parent().get_style_context()
elif view.sibling_left:
ctx = view.sibling_left.get_parent().get_style_context()
lview = view.sibling_left
while lview is not None:
ctx = lview.get_parent().get_style_context()
ctx.remove_class("source-view-focused")
lview = lview.sibling_left
ctx.remove_class("source-view-focused")
rview = view.sibling_right
while rview is not None:
ctx = rview.get_parent().get_style_context()
ctx.remove_class("source-view-focused")
rview = rview.sibling_right

View File

@@ -4,12 +4,19 @@
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from libs.command_system import CommandSystem
from ..source_view import SourceView
from . import commands
class SourceViewCommandSystem(CommandSystem):
def __init__(self):
super(SourceViewCommandSystem, self).__init__(commands)
class CommandSystemMixin:
def toggle_plugins_ui(self):
event = Event_Factory.create_event( "toggle_plugins_ui" )
@@ -81,3 +88,11 @@ class CommandSystemMixin:
)
self.emit_to("completion", event)
def emit(self, event: Code_Event_Types.CodeEvent):
""" Monkey patch 'emit' from command controller... """
...
def emit_to(self, controller: str, event: Code_Event_Types.CodeEvent):
""" Monkey patch 'emit_to' from command controller... """
...

View File

@@ -36,8 +36,11 @@ class ProviderResponseCacheBase:
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_save' not implemented...")
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_change' not implemented...")
def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_text_inserted' not implemented...")
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_delete_range' not implemented...")
def filter(self, word: str) -> list[dict]:
raise ProviderResponseCacheException("ProviderResponseCacheBase 'filter' not implemented...")

View File

@@ -7,7 +7,7 @@ from libs.controllers.controller_base import ControllerBase
from libs.event_factory import Code_Event_Types
from ..command_system import CommandSystem
from ..command_system import SourceViewCommandSystem
@@ -17,14 +17,19 @@ class CommandsController(ControllerBase, list):
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.GetNewCommandSystemEvent):
event.response = self.get_new_command_system()
if isinstance(event, Code_Event_Types.CreateCommandSystemEvent):
event.response = self.create_command_system()
elif isinstance(event, Code_Event_Types.RemovedSourceViewEvent):
self.remove_command_system(event)
def get_new_command_system(self):
command_system = CommandSystem()
def create_command_system(self):
command_system = SourceViewCommandSystem()
command_system.emit = self.emit
command_system.emit_to = self.emit_to
self.append(command_system)
return command_system
def remove_command_system(self, event: Code_Event_Types.RemovedSourceViewEvent):
self.remove(event.view.command)

View File

@@ -41,8 +41,10 @@ class CompletionController(ControllerBase):
self.provider_process_file_close(event)
elif isinstance(event, Code_Event_Types.SavedFileEvent):
self.provider_process_file_save(event)
elif isinstance(event, Code_Event_Types.TextChangedEvent):
self.provider_process_file_change(event)
elif isinstance(event, Code_Event_Types.TextInsertedEvent):
self.provider_process_file_text_inserted(event)
elif isinstance(event, Code_Event_Types.DeleteRangeEvent):
self.provider_process_file_delete_range(event)
elif isinstance(event, Code_Event_Types.RequestCompletionEvent):
self.request_unbound_completion(event)
@@ -88,9 +90,13 @@ class CompletionController(ControllerBase):
for provider in self._providers.values():
provider.response_cache.process_file_save(event)
def provider_process_file_change(self, event: Code_Event_Types.TextChangedEvent):
def provider_process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent):
for provider in self._providers.values():
provider.response_cache.process_file_change(event)
provider.response_cache.process_file_text_inserted(event)
def provider_process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
for provider in self._providers.values():
provider.response_cache.process_file_delete_range(event)
def request_unbound_completion(self, event: Code_Event_Types.RequestCompletionEvent):
completer = event.view.get_completion()

View File

@@ -46,47 +46,57 @@ class MarkerManager(MarkSupportMixin):
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
if is_selection:
self._proc_move(
buffer, is_forward, is_selection, mode, mark_hash,
start_mark, end_mark, has_selection, start_itr, end_itr
)
def _proc_move(
self, buffer, is_forward: bool, is_selection: bool, mode: str,
mark_hash, start_mark, end_mark, has_selection, start_itr, end_itr
):
if is_selection:
if mark_hash:
self.buffer_markers[mark_hash]["is_selection"] = True
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(end_mark, end_itr)
self._apply_selection(buffer, start_itr, end_itr)
continue
if has_selection:
caret_itr = buffer.get_iter_at_mark(end_mark)
start_itr = buffer.get_iter_at_mark(start_mark)
is_left_edge = caret_itr.compare(start_itr) <= 0
is_right_edge = not is_left_edge
can_move = (
(is_forward and is_right_edge) or
(not is_forward and is_left_edge)
)
self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward)
if mode == "word":
if not can_move: continue
itr = caret_itr
self._move_iter(buffer, itr, mode, is_forward)
buffer.move_mark(start_mark, itr)
buffer.move_mark(end_mark, itr)
continue
# No selection - move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr)
buffer.move_mark(end_mark, end_itr)
self._apply_selection(buffer, start_itr, end_itr)
return
if has_selection:
caret_itr = buffer.get_iter_at_mark(end_mark)
start_itr = buffer.get_iter_at_mark(start_mark)
is_left_edge = caret_itr.compare(start_itr) <= 0
is_right_edge = not is_left_edge
can_move = (
(is_forward and is_right_edge) or
(not is_forward and is_left_edge)
)
self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward)
if mode == "word":
if not can_move: return
itr = caret_itr
self._move_iter(buffer, itr, mode, is_forward)
buffer.move_mark(start_mark, itr)
buffer.move_mark(end_mark, itr)
return
# No selection - move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr)
buffer.move_mark(end_mark, end_itr)
def collapse_selection(self,
buffer, mark_hash, start_mark, end_mark, is_forward: bool
):
self.buffer_markers[mark_hash]["is_selection"] = False
if mark_hash:
self.buffer_markers[mark_hash]["is_selection"] = False
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
@@ -105,19 +115,28 @@ class MarkerManager(MarkSupportMixin):
buffer.move_mark(start_mark, collapse_itr)
buffer.move_mark(end_mark, collapse_itr)
def move_word_snake_case(self, itr: Gtk.TextIter, count: int):
def is_word(ch):
def move_along_word(self, itr: Gtk.TextIter, count: int):
def not_is_word(ch: str):
return not is_word(ch)
def is_word(ch: str):
return ch and (ch.isalnum() or ch == "_")
def step(fwd):
def is_special(ch: str):
return ch in "-"
def is_punct(ch: str):
return ch in ".;?!"
def step(fwd: bool):
return itr.forward_cursor_position() if fwd else itr.backward_cursor_position()
def peek(fwd):
def peek(fwd: bool):
if fwd: return itr.get_char()
tmp = itr.copy()
return tmp.backward_cursor_position() and tmp.get_char()
def walk(fwd, cond):
def walk(fwd, cond: callable):
while True:
ch = peek(fwd)
if not cond(ch): break
@@ -126,23 +145,22 @@ class MarkerManager(MarkSupportMixin):
return True
fwd = count > 0
for _ in range( abs(count) ):
ch = peek(fwd)
for _ in range(abs(count)):
ch = itr.get_char() if fwd else peek(False)
if is_word(ch):
# inside word
if is_special(ch) or is_punct(ch):
step(fwd)
elif is_word(ch):
if not walk(fwd, is_word): return
else:
# in separators -> skip them, then the word
if not walk(fwd, lambda c: not is_word(c)): return
if not walk(fwd, not_is_word): return
if not walk(fwd, is_word): return
def _move_iter(self, buffer, itr_, mode: str, is_forward: bool):
if mode == "char":
itr_.forward_char() if is_forward else itr_.backward_char()
elif mode == "word":
self.move_word_snake_case(itr_, 1 if is_forward else -1)
self.move_along_word(itr_, 1 if is_forward else -1)
elif mode == "line":
line = itr_.get_line()
offset = itr_.get_line_offset()

View File

@@ -1,6 +1,9 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.controllers.controller_base import ControllerBase
@@ -29,6 +32,8 @@ class SourceViewsController(ControllerBase, list):
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.CreateSourceViewEvent):
event.response = self.create_source_view(event.state)
elif isinstance(event, Code_Event_Types.RemoveSourceViewEvent):
self._remove_source_view(event)
elif isinstance(event, Code_Event_Types.RemovedFileEvent):
self._remove_file(event)
elif isinstance(event, Code_Event_Types.RegisterCommandEvent):
@@ -86,7 +91,7 @@ class SourceViewsController(ControllerBase, list):
)
def _get_command_system(self):
event = Event_Factory.create_event("get_new_command_system")
event = Event_Factory.create_event("create_command_system")
self.message_to("commands", event)
command = event.response
@@ -103,15 +108,34 @@ class SourceViewsController(ControllerBase, list):
source_view.set_buffer(event.next_file.buffer)
def create_source_view(self, state: SourceViewStates = SourceViewStates.INSERT):
source_view: SourceView = SourceView(state)
source_view.command = self._get_command_system()
source_view.command.set_data(source_view)
def _remove_source_view(self, event: Code_Event_Types.RemovedFileEvent):
event = Event_Factory.create_event("removed_source_view", view = event.view)
self.message(event)
self.remove(event.view)
self.signal_mapper.disconnect_signals(event.view)
def create_source_view(self, state: SourceViewStates = SourceViewStates.INSERT):
scrolled_win: Gtk.ScrolledWindow = Gtk.ScrolledWindow()
source_view: SourceView = SourceView(state)
source_view.command = self._get_command_system()
scrolled_win.set_hexpand(True)
scrolled_win.set_vexpand(True)
source_view.command.set_data(source_view)
self.signal_mapper.connect_signals(source_view)
self.append(source_view)
return source_view
scrolled_win.add(source_view)
scrolled_win.show_all()
event = Event_Factory.create_event(
"created_source_view", view = source_view
)
self.emit(event)
return scrolled_win, source_view
def first_map_load(self):
for source_view in self:

View File

@@ -59,30 +59,56 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
buffer.insert(start_itr, text, -1)
self.marker_manager.apply_to_marks(buffer, replace_word)
return True
def move_cursor(self, source_view, step, count, is_selection, emit):
is_forward = count > 0
buffer = source_view.get_buffer()
def move_cursor(
self, source_view, step, count, is_selection,
emit = None, ignore_leader: bool = False
):
is_forward = count > 0
buffer = source_view.get_buffer()
if step in [
Gtk.MovementStep.LOGICAL_POSITIONS,
Gtk.MovementStep.VISUAL_POSITIONS
]:
self.marker_manager.move_by_char(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.WORDS:
self.marker_manager.move_by_word(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.DISPLAY_LINES:
self.marker_manager.move_by_line(buffer, is_forward, is_selection)
start_mark = buffer.get_insert()
end_mark = buffer.get_selection_bound()
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
has_selection = not start_itr.equal(end_itr)
self._signal_cursor_moved(source_view, emit)
step_map = {
Gtk.MovementStep.LOGICAL_POSITIONS: ("char", self.marker_manager.move_by_char),
Gtk.MovementStep.VISUAL_POSITIONS: ("char", self.marker_manager.move_by_char),
Gtk.MovementStep.WORDS: ("word", self.marker_manager.move_by_word),
Gtk.MovementStep.DISPLAY_LINES: ("line", self.marker_manager.move_by_line),
}
return False
kind, move_fn = step_map[step]
move_fn(buffer, is_forward, is_selection)
if ignore_leader: return True
self.marker_manager._proc_move(
buffer,
is_forward,
is_selection,
kind,
None,
start_mark,
end_mark,
has_selection,
start_itr,
end_itr,
)
# self._signal_cursor_moved(source_view, emit)
return True
def key_press_event(self, source_view, event, key_mapper):
char = key_mapper.get_raw_keyname(event).upper()
self.is_control = key_mapper.is_control(event)
self.is_shift = key_mapper.is_shift(event)
self.is_super = key_mapper.is_super(event)
if char.upper() in ["BACKSPACE", "DELETE", "ENTER"]:
self.marker_manager.process_cursor_action(
@@ -91,6 +117,9 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
)
return False
if self._do_cursor_moved(source_view, char):
return True
return super().key_press_event(source_view, event, key_mapper)
def button_press_event(self, source_view, event):
@@ -99,6 +128,56 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
def button_release_event(self, source_view, event):
self.marker_manager.button_release_event(source_view, event)
def _do_cursor_moved(self, source_view, char: str):
key = char.upper()
if key not in {"LEFT", "RIGHT", "UP", "DOWN"}: return False
direction = {
"LEFT": -1,
"RIGHT": 1,
"UP": -1,
"DOWN": 1,
}[key]
is_horizontal = key in {"LEFT", "RIGHT"}
step = \
Gtk.MovementStep.VISUAL_POSITIONS if is_horizontal else Gtk.MovementStep.DISPLAY_LINES
count = direction
is_selection = self.is_shift
if is_horizontal:
if self.is_control:
step = Gtk.MovementStep.WORDS
if self.is_control and self.is_shift:
is_selection = True
if self.is_super:
return self.move_cursor(
source_view,
step,
count,
is_selection = False,
emit = None,
ignore_leader = True,
)
else:
if self.is_super:
return self.move_cursor(
source_view,
step,
count,
is_selection,
emit = None,
ignore_leader = True,
)
return self.move_cursor(
source_view,
step = step,
count = count,
is_selection = is_selection,
)
def _signal_cursor_moved(self, source_view, emit):
buffer = source_view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )

View File

@@ -145,7 +145,7 @@ class KeyMapper:
is_shift, \
is_alt = self.get_modkeys_states(eve)
self.state = NoKeyState
self.state = NoKeyState
if is_control:
self.state = self.state | CtrlKeyState
if is_shift:
@@ -161,6 +161,10 @@ class KeyMapper:
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
return modifiers & Gdk.ModifierType.SHIFT_MASK
def is_super(self, eve):
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
return modifiers & Gdk.ModifierType.SUPER_MASK
def get_raw_keyname(self, eve) -> str:
return Gdk.keyval_name(eve.keyval)

View File

@@ -31,6 +31,7 @@ class SourceBuffer(GtkSource.Buffer):
_insert_text,
_after_insert_text,
_modified_changed,
_delete_range,
):
self._handler_ids = [
@@ -39,7 +40,8 @@ class SourceBuffer(GtkSource.Buffer):
self.connect("mark-set", _mark_set),
self.connect("insert-text", _insert_text),
self.connect_after("insert-text", _after_insert_text),
self.connect("modified-changed", _modified_changed)
self.connect("modified-changed", _modified_changed),
self.connect("delete-range", _delete_range)
]
def block_changed_signal(self):
@@ -54,6 +56,9 @@ class SourceBuffer(GtkSource.Buffer):
def block_modified_changed_signal(self):
self.handler_block(self._handler_ids[5])
def block_delete_range(self):
self.handler_block(self._handler_ids[6])
def unblock_changed_signal(self):
self.handler_unblock(self._handler_ids[0])
@@ -66,6 +71,9 @@ class SourceBuffer(GtkSource.Buffer):
def unblock_modified_changed_signal(self):
self.handler_unblock(self._handler_ids[5])
def unblock_delete_range(self):
self.handler_block(self._handler_ids[6])
def clear_signals(self):
for handle_id in self._handler_ids:
self.disconnect(handle_id)

View File

@@ -40,7 +40,8 @@ class SourceFile(GtkSource.File):
self._mark_set,
self._insert_text,
self._after_insert_text,
self._modified_changed
self._modified_changed,
self._delete_range
)
def _changed(self, buffer: SourceBuffer):
@@ -60,7 +61,20 @@ class SourceFile(GtkSource.File):
location: Gtk.TextIter,
text: str, length: int
):
...
event = Event_Factory.create_event(
"text_insert",
file = self,
buffer = self.buffer,
location = location,
text = text,
length = length
)
# Note: 'idle_add' needed b/c markers don't get thir positions
# updated relative to the initial insert.
# If not used, seg faults galor during multi insert.
# GLib.idle_add(self.emit, event)
self.emit(event)
def _after_insert_text(
self,
@@ -99,7 +113,19 @@ class SourceFile(GtkSource.File):
def _modified_changed(self, buffer: SourceBuffer):
event = Event_Factory.create_event(
"modified_changed",
file = self, buffer = buffer
file = self,
buffer = buffer
)
self.emit(event)
def _delete_range(self, buffer: SourceBuffer, start: Gtk.TextIter, end: Gtk.TextIter):
event = Event_Factory.create_event(
"delete_range",
file = self,
buffer = buffer,
start = start,
end = end,
)
self.emit(event)
@@ -155,9 +181,10 @@ class SourceFile(GtkSource.File):
self.buffer.unblock_modified_changed_signal()
def is_externally_modified(self) -> bool:
stat = os.stat(self.fpath)
current = (stat.st_mtime_ns, stat.st_size)
if self.fname == "buffer": return
stat = os.stat(self.fpath)
current = (stat.st_mtime_ns, stat.st_size)
is_modified = \
hasattr(self, "last_state") and not current == self.last_state

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