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

View File

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

19
TODO.md
View File

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

BIN
images/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
images/pic3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/pic4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/pic5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

BIN
images/pic6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

View File

@@ -14,53 +14,94 @@ class Commenter(CodeCommentTagsMixin):
def keyboard_tggl_comment(self, buffer): def keyboard_tggl_comment(self, buffer):
language = buffer.get_language() language = buffer.get_language()
if language is None: return if not language: return
start_tag, end_tag = self.get_comment_tags(language) start_tag, end_tag = self.get_comment_tags(language)
# Note: Only handling line comment tag- no block comment option if not (start_tag or end_tag): return
if not start_tag and not end_tag: return
start_tag += " "
end_tag = end_tag or ""
bounds = buffer.get_selection_bounds() bounds = buffer.get_selection_bounds()
if bounds:
self._bounds_comment( (self._bounds_comment if bounds else self._line_comment)(
start_tag, end_tag, bounds, buffer 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: else:
self._line_comment(start_tag, end_tag, buffer) stripped = f"{start_tag}{stripped}{end_tag}"
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.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, indent + stripped)
buffer.end_user_action() buffer.end_user_action()
buffer.place_cursor(buffer.get_iter_at_line_offset(line, col))
def _bounds_comment(self, start_tag, end_tag, bounds, buffer): def _bounds_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start_itr, end_itr = bounds def indent_len(s): return len(s) - len(s.lstrip())
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) def insert(line, idx):
text = "\n".join( return f"{line[:idx]}{start_tag}{line[idx:]}{end_tag}"
line.replace(start_tag, "") if line.startswith(start_tag) else start_tag + line
for line in text.splitlines() 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.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, new_text)
buffer.end_user_action() 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

@@ -25,7 +25,7 @@ class Plugin(PluginCode):
if len(history) == history_size: if len(history) == history_size:
history.pop(0) history.pop(0)
history.append(event.file) history.append(event.file.fpath)
def load(self): def load(self):
self._manage_signals("register_command") self._manage_signals("register_command")
@@ -60,6 +60,6 @@ class Handler:
view._on_uri_data_received( view._on_uri_data_received(
[ [
history.pop().replace("file://", "") f"file://{history.pop()}"
] ]
) )

View File

@@ -14,16 +14,24 @@ from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler: class Handler:
@staticmethod @staticmethod
def execute( def execute(view: GtkSource.View, *args, **kwargs):
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Cut to Temp Buffer") logger.debug("Command: Cut to Temp Buffer")
clear_temp_cut_buffer_delayed(view) clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer() buffer = view.get_buffer()
if buffer.get_has_selection():
start_itr, end_itr = buffer.get_selection_bounds()
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()) itr = buffer.get_iter_at_mark(buffer.get_insert())
start_itr = itr.copy() start_itr = itr.copy()
@@ -36,9 +44,14 @@ class Handler:
if not hasattr(view, "_cut_buffer"): if not hasattr(view, "_cut_buffer"):
view._cut_buffer = "" view._cut_buffer = ""
line_str = buffer.get_text(start_itr, end_itr, True) text = buffer.get_text(start_itr, end_itr, True)
view._cut_buffer += line_str
if not text.endswith("\n"):
text += "\n"
view._cut_buffer += text
buffer.delete(start_itr, end_itr) 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

@@ -23,7 +23,7 @@ class Plugin(PluginCode):
def load(self): def load(self):
self._manage_signals("register_command") self._manage_signals("register_command")
def load(self): def unload(self):
self._manage_signals("unregister_command") self._manage_signals("unregister_command")
def _manage_signals(self, action: str): def _manage_signals(self, action: str):

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

View File

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

View File

@@ -1,5 +1,5 @@
{ {
"name": "Toggle Source View", "name": "Split Pane",
"author": "ITDominator", "author": "ITDominator",
"version": "0.0.1", "version": "0.0.1",
"support": "", "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_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]: 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_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]: 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_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]: 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_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):
buffer = event.file.buffer buffer = event.buffer
self._clear_temp_delay() self._clear_temp_delay()
self._set_temp_delay(buffer) self._set_temp_delay(buffer)
def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent):
...
def _clear_temp_delay(self): def _clear_temp_delay(self):
if self._temp_timeout_id: if self._temp_timeout_id:
GLib.source_remove(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): def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.TextChangedEvent): if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
event.file.check_file_on_disk() event = Event_Factory.create_event(
"get_file", buffer = event.view.get_buffer()
)
self.emit_to("files", event)
if event.file.is_deleted(): file = event.response
file_is_deleted(event, self.emit) if not file: return
elif event.file.is_externally_modified(): if file.ftype == "buffer": return
file_is_externally_modified(event, self.emit)
file.check_file_on_disk()
if file.is_deleted():
file_is_deleted(file, self.emit)
elif file.is_externally_modified():
file_is_externally_modified(file, self.emit)
def load(self): def load(self):
... ...

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .tree_sitter import Parser, get_parser
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def 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.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 == "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):
...
def unload(self):
...
def run(self):
...

View File

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

View File

@@ -7,7 +7,7 @@
"socket": "ws://127.0.0.1:9999/gdscript", "socket": "ws://127.0.0.1:9999/gdscript",
"socket-two": "ws://127.0.0.1:9999/?name=gdscript", "socket-two": "ws://127.0.0.1:9999/?name=gdscript",
"initialization-options": { "initialization-options": {
"processId": , "processId": null,
"clientInfo": { "clientInfo": {
"name": "Godot", "name": "Godot",
"version": "4.4" "version": "4.4"

View File

@@ -11,7 +11,7 @@ from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
from .response_handler import GodotHandler from .response_handler import GDScriptHandler
@@ -30,7 +30,7 @@ class Plugin(PluginCode):
event = Event_Factory.create_event("register_lsp_client", event = Event_Factory.create_event("register_lsp_client",
lang_id = "gdscript", lang_id = "gdscript",
lang_config = config, lang_config = config,
handler = GodotHandler handler = GDScriptHandler
) )
self.emit_to("lsp_manager", event) self.emit_to("lsp_manager", event)

View File

@@ -1 +1 @@
from .python import PythonHandler from .gdscript import GDScriptHandler

View File

@@ -7,6 +7,6 @@ from lsp_manager.response_handlers.default import DefaultHandler
class GodotHandler(DefaultHandler): class GDScriptHandler(DefaultHandler):
"""Uses default handling, can override if Godot needs special logic.""" """Uses default handling, can override if Godot needs special logic."""
... ...

View File

@@ -1,13 +1,9 @@
# Python imports # Python imports
import threading
from os import path
import json
# Lib imports # Lib imports
import gi
from gi.repository import GLib
# Application imports # Application imports
from ..config import get_lsp_init_config
from ..dto.code.lsp.lsp_messages import get_message_str from ..dto.code.lsp.lsp_messages import get_message_str
from ..dto.code.lsp.lsp_message_structs import \ from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification LSPResponseTypes, ClientRequest, ClientNotification
@@ -19,38 +15,30 @@ class LSPClient(LSPClientWebsocket):
def __init__(self): def __init__(self):
super(LSPClient, self).__init__() super(LSPClient, self).__init__()
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers self._socket: str = ""
# initialize-params-slim.json was created off of jedi_language_server one
# self._init_params = settings_manager.get_lsp_init_data()
self._language: str = "" self._language: str = ""
self._init_params: dict = {} self._workspace_path: str = ""
self._event_history: dict[int, str] = {}
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._message_id: int = -1
self._socket = None self._event_history: dict[int, str] = {}
self.read_lock = threading.Lock() self._init_params: dict = get_lsp_init_config()
self.write_lock = threading.Lock() self._init_opts: dict[str, str] = {}
self.doc_vers: dict[str, int] = {}
def set_language(self, language: str): def set_language(self, language: str):
self._language = language 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): def set_socket(self, socket: str):
self._socket = socket self._socket = socket
def unset_socket(self): def unset_socket(self):
self._socket = None self._socket = ""
def send_notification(self, method: str, params: dict = {}): def send_notification(self, method: str, params: dict = {}):
self._send_message( ClientNotification(method, params) ) self._send_message( ClientNotification(method, params) )
@@ -64,5 +52,5 @@ class LSPClient(LSPClientWebsocket):
if not message_id in self._event_history: return if not message_id in self._event_history: return
return self._event_history[message_id] return self._event_history[message_id]
def handle_lsp_response(self, lsp_response: LSPResponseTypes): def handle_lsp_response(self, lsp_response: LSPResponseTypes | dict):
raise NotImplementedError 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 didsave_notification
from ..dto.code.lsp.lsp_messages import didclose_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
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 completion_request
from ..dto.code.lsp.lsp_messages import definition_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 references_request
from ..dto.code.lsp.lsp_messages import symbols_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: class LSPClientEvents:
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str): def send_initialize_message(self):
folder_name = os.path.basename(workspace_file) folder_name = os.path.basename(self._workspace_path)
workspace_uri = f"file://{self._workspace_path}"
self._init_params["processId"] = None 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["rootUri"] = workspace_uri
self._init_params["workspaceFolders"] = [ 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) self.send_request("initialize", self._init_params)
def send_initialized_message(self): def send_initialized_notification(self):
self.send_notification("initialized") 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): def _lsp_did_open(self, data: dict):
method = "textDocument/didOpen" method = "textDocument/didOpen"
params = didopen_notification["params"] params = didopen_notification["params"]
self.doc_vers[ data["uri"] ] = -1
params["textDocument"]["uri"] = data["uri"] params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"] params["textDocument"]["languageId"] = data["language_id"]
@@ -76,24 +88,24 @@ class LSPClientEvents:
self.send_notification( method, params ) self.send_notification( method, params )
# def _lsp_did_change(self, data: dict): def _lsp_did_change_range(self, data: dict):
# method = "textDocument/didChange" method = "textDocument/didChange"
# params = didchange_notification_range["params"] params = didchange_notification_range["params"]
# params["textDocument"]["uri"] = data["uri"] params["textDocument"]["uri"] = data["uri"]
# params["textDocument"]["languageId"] = data["language_id"] params["textDocument"]["languageId"] = data["language_id"]
# params["textDocument"]["version"] = data["version"] params["textDocument"]["version"] = data["version"]
# contentChanges = params["contentChanges"][0] contentChanges = params["contentChanges"][0]
# start = contentChanges["range"]["start"] start = contentChanges["range"]["start"]
# end = contentChanges["range"]["end"] end = contentChanges["range"]["end"]
# contentChanges["text"] = data["text"] contentChanges["text"] = data["text"]
# start["line"] = data["line"] start["line"] = data["line"]
# start["character"] = 0 start["character"] = data["column"]
# end["line"] = data["line"] end["line"] = data["end_line"]
# end["character"] = data["column"] end["character"] = data["end_column"]
# self.send_notification( method, params ) self.send_notification( method, params )
def _lsp_definition(self, data: dict): def _lsp_definition(self, data: dict):
method = "textDocument/definition" method = "textDocument/definition"
@@ -107,13 +119,33 @@ class LSPClientEvents:
self.send_request( method, params ) 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): def _lsp_completion(self, data: dict):
method = "textDocument/completion" method = "textDocument/completion"
params = completion_request["params"] params = completion_request["params"]
params["textDocument"]["uri"] = data["uri"] params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"] params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"] params["position"]["character"] = data["column"]

View File

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

View File

@@ -4,14 +4,15 @@ from concurrent.futures import ThreadPoolExecutor
# Lib imports # Lib imports
# Application 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 from .client.lsp_client import LSPClient
class LSPManagerClient(LSPClientEventsMixin): class ClientManager(ClientManagerEventsMixin):
def __init__(self): def __init__(self):
super(LSPManagerClient, self).__init__() super(ClientManager, self).__init__()
self._cache_refresh_timeout_id: int = None self._cache_refresh_timeout_id: int = None
@@ -22,22 +23,24 @@ class LSPManagerClient(LSPClientEventsMixin):
def create_client( def create_client(
self, self,
lang_id: str = "python", lang_id: str,
workspace_uri: str = "", workspace_path: str,
init_opts: dict = {} init_opts: dict[str, str],
address: str = "127.0.0.1",
port: str = "9999"
) -> LSPClient: ) -> LSPClient:
if lang_id in self.clients: return None if lang_id in self.clients: return None
address = "127.0.0.1" uri = f"ws://{address}:{port}/{lang_id}?workspace={workspace_path}"
port = 9999
uri = f"ws://{address}:{port}/{lang_id}"
client = LSPClient() client = LSPClient()
client.set_language(lang_id)
client.set_socket(uri) client.set_socket(uri)
client.set_language(lang_id)
client.set_workspace_path(workspace_path)
client.set_init_opts(init_opts)
client.start_client() 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}") logger.error(f"Failed to connect to LSP server for {lang_id}")
return None 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://", "uri": "file://",
"languageId": "python", "languageId": "python",
"version": 1, "version": 1,
"text": ""
}, },
"contentChanges": [ "contentChanges": [
{ {
"text": "",
"range": { "range": {
"start": { "start": {
"line": 1, "line": 1,
@@ -108,9 +108,8 @@ didchange_notification_range = {
"end": { "end": {
"line": 1, "line": 1,
"character": 1, "character": 1,
},
"rangeLength": 0
} }
},
} }
] ]
} }
@@ -125,19 +124,11 @@ completion_request = {
"method": "textDocument/completion", "method": "textDocument/completion",
"params": { "params": {
"textDocument": { "textDocument": {
"uri": "file://", "uri": "file://"
"languageId": "python",
"version": 1,
"text": ""
}, },
"position": { "position": {
"line": 5, "line": 5,
"character": 12, "character": 12
"offset": 0
},
"contet": {
"triggerKind": 3,
"triggerCharacter": ""
} }
} }
} }
@@ -159,6 +150,19 @@ definition_request = {
} }
} }
implementation_request = {
"method": "textDocument/implementation",
"params": {
"textDocument": {
"uri": "file://"
},
"position": {
"line": 5,
"character": 12
}
}
}
references_request = { references_request = {
"method": "textDocument/references", "method": "textDocument/references",
"params": { "params": {
@@ -179,7 +183,6 @@ references_request = {
} }
} }
symbols_request = { symbols_request = {
"method": "textDocument/documentSymbol", "method": "textDocument/documentSymbol",
"params": { "params": {
@@ -191,3 +194,14 @@ symbols_request = {
} }
} }
} }
shutdown_request = {
"method": "shutdown",
"params": None
}
exit_request = {
"method": "exit",
"params": None
}

View File

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

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

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 plugins.plugin_types import PluginCode
from .dto.code import events as lsp_events from .dto.code import events as lsp_events
from .commands import Commands
from .lsp_manager import LSPManager from .lsp_manager import LSPManager
@@ -35,23 +36,11 @@ class Plugin(PluginCode):
window = self.request_ui_element("main-window") 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", self._manage_signals("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)
event = Event_Factory.create_event( self._manage_provider("register_provider")
"register_provider",
provider_name = "LSP Completer",
provider = lsp_manager.provider,
language_ids = []
)
self.emit_to("completion", event)
event = Event_Factory.create_event( event = Event_Factory.create_event(
"create_source_view", "create_source_view",
@@ -59,8 +48,8 @@ class Plugin(PluginCode):
) )
self.emit_to("source_views", event) self.emit_to("source_views", event)
source_view = event.response scrolled_win, source_view = event.response
lsp_manager.lsp_manager_ui.set_source_view(source_view) lsp_manager.ui_manager.set_source_view(scrolled_win, source_view)
def unload(self): def unload(self):
Event_Factory.unregister_events( lsp_events.__dict__.items() ) Event_Factory.unregister_events( lsp_events.__dict__.items() )
@@ -69,56 +58,61 @@ class Plugin(PluginCode):
window = self.request_ui_element("main-window") 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", self._manage_signals("unregister_command")
command_name = "LSP Manager",
command = Handler, 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_mode = "released",
binding = ["<Shift><Control>l", "<Control>g", "<Control>i"] binding = "<Shift><Control>l"
) )
self.emit_to("source_views", event) self.emit_to("source_views", event)
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( event = Event_Factory.create_event(
"unregister_provider", action,
provider_name = "LSP Completer" provider_name = "LSP Completer",
provider = lsp_manager.provider,
language_ids = []
) )
self.emit_to("completion", event) self.emit_to("completion", event)
lsp_manager.handle_destroy()
def run(self): def run(self):
... ...
def generate_plugin_element(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" return "LSP Code Completion"
def do_match(self, context): def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
# completion = context.get_property("completion")
# if not completion.get_view().has_focus(): return
iter = self.response_cache.get_iter_correctly(context) iter = self.response_cache.get_iter_correctly(context)
iter.backward_char()
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() buffer = iter.get_buffer()
if buffer.get_context_classes_at_iter(iter) != ['no-spell-check']: 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.NONE
return GtkSource.CompletionActivation.USER_REQUESTED return GtkSource.CompletionActivation.USER_REQUESTED
# return GtkSource.CompletionActivation.INTERACTIVE # return GtkSource.CompletionActivation.INTERACTIVE
# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE
def do_populate(self, context): def do_populate(self, context):
results = self.response_cache.filter_with_context(context) results = self.response_cache.filter_with_context(context)

View File

@@ -33,9 +33,13 @@ class ProviderResponseCache(ProviderResponseCacheBase):
if self.lsp_manager_client: if self.lsp_manager_client:
self.lsp_manager_client.process_file_save(event) 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: 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]: def filter(self, word: str) -> list[dict]:
return [] return []

View File

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

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": { "jedi_completion": {
"enabled": true, "enabled": true,
"fuzzy": true,
"include_params": false,
"include_class_objects": true, "include_class_objects": true,
"include_function_objects": true, "include_function_objects": true
"fuzzy": false },
"jedi_signature_help": {
"enabled": true
},
"jedi_references": {
"enabled": true
}, },
"jedi": { "jedi": {
"root_dir": "file://{workspace.folder}", "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") event = Event_Factory.create_event("get_active_view")
self.emit_to("source_views", event) self.emit_to("source_views", event)
if not event.response: return
code_minimap.set_smini_view(event.response) code_minimap.set_smini_view(event.response)
def unload(self): def unload(self):

View File

@@ -99,7 +99,7 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin):
def _tggle_preview_updates(self, widget): def _tggle_preview_updates(self, widget):
self.is_preview_paused = not self.is_preview_paused 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_hide)
self.disconnect_by_func(self._handle_show) self.disconnect_by_func(self._handle_show)
self.disconnect_by_func(self._handle_destroy) self.disconnect_by_func(self._handle_destroy)

View File

@@ -41,18 +41,14 @@ class Plugin(PluginCode):
def load(self): def load(self):
separator_right = self.request_ui_element("separator-right") separator_right = self.request_ui_element("separator-right")
markdown_preview.set_relative_to(separator_right) markdown_preview.set_relative_to(separator_right)
self._manage_signals("register_command")
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)
def unload(self): 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_name = "tggle_markdown_preview",
command = Handler, command = Handler,
binding_mode = "released", binding_mode = "released",
@@ -61,8 +57,6 @@ class Plugin(PluginCode):
self.emit_to("source_views", event) self.emit_to("source_views", event)
markdown_preview.destroy()
def run(self): def run(self):
... ...

View File

@@ -1,6 +1,9 @@
# Python imports # Python imports
# Lib imports # Lib imports
import gi
from gi.repository import Gtk
# Application imports # Application imports
from libs.event_factory import Event_Factory, Code_Event_Types from libs.event_factory import Event_Factory, Code_Event_Types
@@ -25,8 +28,19 @@ class Plugin(PluginCode):
self.register_controller("tabs", self.tabs_controller) self.register_controller("tabs", self.tabs_controller)
code_container.add( self.tabs_controller.tabs_widget ) scrolled_win = Gtk.ScrolledWindow()
code_container.reorder_child(self.tabs_controller.tabs_widget, 0) viewport = Gtk.Viewport()
scrolled_win.set_overlay_scrolling(False)
scrolled_win.set_size_request(-1, 50)
viewport.add( self.tabs_controller.tabs_widget )
scrolled_win.add( viewport )
code_container.add( scrolled_win )
code_container.reorder_child(scrolled_win, 0)
viewport.show()
scrolled_win.show()
event = Event_Factory.create_event("get_files") event = Event_Factory.create_event("get_files")
self.emit_to("files", event) self.emit_to("files", event)
@@ -36,7 +50,14 @@ class Plugin(PluginCode):
def unload(self): def unload(self):
self.unregister_controller("tabs") self.unregister_controller("tabs")
self.tabs_controller.unload_tabs() self.tabs_controller.unload_tabs()
self.tabs_controller.tabs_widget.destroy()
tabs_widget = self.tabs_controller.tabs_widget
viewport = tabs_widget.get_parent()
scrolled_win = viewport.get_parent()
tabs_widget.destroy()
viewport.destroy()
scrolled_win.destroy()
self.tabs_controller.tabs_widget = None self.tabs_controller.tabs_widget = None
self.tabs_controller = None self.tabs_controller = None

View File

@@ -27,7 +27,7 @@ class TabsWidget(Gtk.Notebook):
def _setup_styling(self): def _setup_styling(self):
self.set_scrollable(True) ...
def _setup_signals(self): def _setup_signals(self):
self.connect("page-added", self._page_added) self.connect("page-added", self._page_added)
@@ -67,6 +67,7 @@ class TabsWidget(Gtk.Notebook):
) )
self.emit(event) self.emit(event)
self._scroll_to_center(tab)
def _bind_tab_menu(self, tab, page_widget): def _bind_tab_menu(self, tab, page_widget):
def do_context_menu(tab, eve, page_widget): def do_context_menu(tab, eve, page_widget):
@@ -81,6 +82,21 @@ class TabsWidget(Gtk.Notebook):
page_widget page_widget
) )
def _scroll_to_center(self, tab):
scrolled_win = self.get_parent().get_parent()
alloc = tab.get_allocation()
tab_x = alloc.x
tab_width = alloc.width
view_width = scrolled_win.get_allocated_width()
target = tab_x + tab_width / 2 - view_width / 2
adj = scrolled_win.get_hadjustment()
lower = adj.get_lower()
upper = adj.get_upper()
page_size = adj.get_page_size()
target = max(lower, min(target, upper - page_size))
adj.set_value(target)
def create_menu(self, page_widget) -> Gtk.Menu: def create_menu(self, page_widget) -> Gtk.Menu:
context_menu = Gtk.Menu() context_menu = Gtk.Menu()
close_submenu = Gtk.Menu() close_submenu = Gtk.Menu()
@@ -130,6 +146,7 @@ class TabsWidget(Gtk.Notebook):
self.page_num(page_widget) self.page_num(page_widget)
) )
self.handler_unblock(self.switch_page_id) self.handler_unblock(self.switch_page_id)
self._scroll_to_center(tab)
break break

View File

@@ -45,6 +45,7 @@ class ListBox(Gtk.ListBox):
def _row_activated(self, list_box, row = None): def _row_activated(self, list_box, row = None):
row = self.get_selected_row() row = self.get_selected_row()
if not row: return
file = row.get_children()[0].file file = row.get_children()[0].file
event = Event_Factory.create_event( event = Event_Factory.create_event(
@@ -70,25 +71,63 @@ class ListBox(Gtk.ListBox):
def search_changed(self, entry): def search_changed(self, entry):
self.search_buffer_names(entry) self.search_buffer_names(entry)
for row in self.get_children(): for row in self.get_children():
if not row.is_visible(): continue if not row.is_visible(): continue
self.select_row(row) self.select_row(row)
break
def fuzzy_score(self, query, text):
query = query.lower()
text = text.lower()
score = 0
q_idx = 0
for char in text:
if q_idx < len(query) and char == query[q_idx]:
score += 1
q_idx += 1
return score if q_idx == len(query) else 0
def search_buffer_names(self, entry): def search_buffer_names(self, entry):
text = entry.get_text() query = entry.get_text().lower()
if not text: rows = []
for row in self.get_children():
row.show()
return
for row in self.get_children(): for row in self.get_children():
child = row.get_children()[0] child = row.get_children()[0]
label_text = child.get_label()
score = self.fuzzy_score(query, label_text) if query else 1
child.label_text = label_text if not hasattr(child, "label_text") else child.label_text
row.show() \ rows.append((score, row, child, label_text))
if text in child.get_label() else \
rows.sort(key = lambda x: x[0], reverse = True)
for score, row, child, label_text in rows:
if query and score > 0:
highlighted = self.highlight_match(label_text, query)
child.set_markup(highlighted)
row.show()
elif not query:
child.set_label(child.label_text)
row.show()
else:
row.hide() row.hide()
def highlight_match(self, text, query):
i = 0
result = ""
for char in text:
if i < len(query) and char.lower() == query[i].lower():
result += f"<b><i><u>{char}</u></i></b>"
i += 1
else:
result += char
return result
def activate_row(self): def activate_row(self):
self._row_activated(self) self._row_activated(self)
@@ -97,29 +136,43 @@ class ListBox(Gtk.ListBox):
def move_row_selection_up(self): def move_row_selection_up(self):
row = self.get_selected_row() row = self.get_selected_row()
next_row = self.get_row_at_index(row.get_index() - 1) if not row: return
if not next_row: rows = [r for r in self.get_children() if r.is_visible()]
next_row = self.get_row_at_index( if not rows: return
len( self.get_children() ) - 1
)
self.select_row(next_row) try:
idx = rows.index(row)
except ValueError:
return
next_idx = (idx - 1) % len(rows)
self.select_row(rows[next_idx])
def move_row_selection_down(self): def move_row_selection_down(self):
row = self.get_selected_row() row = self.get_selected_row()
next_row = self.get_row_at_index(row.get_index() + 1) if not row: return
if not next_row: rows = [r for r in self.get_children() if r.is_visible()]
next_row = self.get_row_at_index(0) if not rows: return
self.select_row(next_row) try:
idx = rows.index(row)
except ValueError:
return
next_idx = (idx + 1) % len(rows)
self.select_row(rows[next_idx])
def add_row(self, file): def add_row(self, file):
row = Gtk.ListBoxRow()
label = Gtk.Label(label = file.fname) label = Gtk.Label(label = file.fname)
label.file = file label.file = file
label.show()
self.add(label) row.add(label)
row.show_all()
self.add(row)
def remove_row(self, event): def remove_row(self, event):
for row in self.get_children(): for row in self.get_children():

View File

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

View File

@@ -99,13 +99,9 @@ class Telescope(Gtk.Dialog):
def unmap_parent_resize_event(self, parent): def unmap_parent_resize_event(self, parent):
parent.disconnect(self.size_allocate_id) parent.disconnect(self.size_allocate_id)
def set_source_view(self, source_view): def set_source_view(self, scrolled_win, source_view):
scrolled_win = Gtk.ScrolledWindow()
self.source_view = source_view self.source_view = source_view
scrolled_win.add(self.source_view)
self.main_box.pack_end(scrolled_win, True, True, 0) self.main_box.pack_end(scrolled_win, True, True, 0)
scrolled_win.show_all() scrolled_win.show_all()
def _handle_destroy(self, widget): 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 from gi.repository import Gtk
# Application imports # 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 .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 from gi.repository import Gtk
# Application imports # Application imports
from ...widgets.code.code_base import CodeBase
from ...widgets.separator_widget import Separator
from .editors_container import EditorsContainer from .editors_container import EditorsContainer
@@ -27,6 +23,9 @@ class CodeContainer(Gtk.Box):
def _setup_styling(self): def _setup_styling(self):
self.ctx = self.get_style_context()
self.ctx.add_class("code-container")
self.set_orientation(Gtk.Orientation.VERTICAL) self.set_orientation(Gtk.Orientation.VERTICAL)
def _setup_signals(self): def _setup_signals(self):
@@ -37,18 +36,4 @@ class CodeContainer(Gtk.Box):
def _load_widgets(self): def _load_widgets(self):
widget_registery.expose_object("code-container", self) widget_registery.expose_object("code-container", self)
self.add( EditorsContainer() )
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

View File

@@ -4,18 +4,17 @@
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
#from gi.repository import GLib
# Application imports # Application imports
from core.widgets.code.code_base import CodeBase
from core.widgets.separator_widget import Separator
class EditorsContainer(Gtk.Paned): class EditorsContainer(Gtk.Box):
def __init__(self, code_base: any): def __init__(self):
super(EditorsContainer, self).__init__() super(EditorsContainer, self).__init__()
self.code_base = code_base
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
self._subscribe_to_events() self._subscribe_to_events()
@@ -24,11 +23,11 @@ class EditorsContainer(Gtk.Paned):
def _setup_styling(self): def _setup_styling(self):
self.ctx = self.get_style_context() 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_hexpand(True)
self.set_vexpand(True) self.set_vexpand(True)
self.set_wide_handle(True) self.set_size_request(320, -1)
def _setup_signals(self): def _setup_signals(self):
self.connect("map", self._init_map) self.connect("map", self._init_map)
@@ -37,29 +36,18 @@ class EditorsContainer(Gtk.Paned):
... ...
def _load_widgets(self): def _load_widgets(self):
self.scrolled_win1, \ box = Gtk.Box()
self.scrolled_win2 = self._create_views() 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 ) box.add( scrolled_win )
self.pack2( self.scrolled_win2, True, True ) self.add( Separator("separator_left") )
self.add( box )
def _create_views(self): self.add( Separator("separator_right") )
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
def _init_map(self, view): def _init_map(self, view):
self.disconnect_by_func( self._init_map ) self.disconnect_by_func( self._init_map )
self.code_base.first_map_load() self.code_base.first_map_load()
self.code_base = None
del self.code_base del self.code_base

View File

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

View File

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

View File

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

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