refactor(code): move temp cut buffer feature and clean up related code
- Moved cut_to_temp_buffer and paste_temp_buffer commands to plugin - Remove temporary cut buffer logic from SourceView - Remove related keybindings (Ctrl+K / Ctrl+U) - Fix word-end detection in ProviderResponseCacheBase to avoid extending past line end - Improve completion controller to support RequestCompletionEvent with optional provider filtering - Add GetActiveViewEvent handling in SourceViewsController - Ensure view scrolls to cursor after buffer switch via GLib.idle_add - Emit focus-in-event after DnD file load to restore focus behavior - Prevent "buffer" pseudo-path from interfering with loaded file filtering - Remove obsolete AGENTS.md from LSP manager plugin
This commit is contained in:
3
plugins/code/commands/nanoesq_temp_buffer/__init__.py
Normal file
3
plugins/code/commands/nanoesq_temp_buffer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
||||
3
plugins/code/commands/nanoesq_temp_buffer/__main__.py
Normal file
3
plugins/code/commands/nanoesq_temp_buffer/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
||||
@@ -0,0 +1,44 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
||||
|
||||
|
||||
|
||||
class Handler:
|
||||
@staticmethod
|
||||
def execute(
|
||||
view: GtkSource.View,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
logger.debug("Command: Cut to Temp Buffer")
|
||||
|
||||
clear_temp_cut_buffer_delayed(view)
|
||||
|
||||
buffer = view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
||||
|
||||
start_itr = itr.copy()
|
||||
start_itr.set_line_offset(0)
|
||||
|
||||
end_itr = start_itr.copy()
|
||||
if not end_itr.forward_line():
|
||||
end_itr = buffer.get_end_iter()
|
||||
|
||||
if not hasattr(view, "_cut_buffer"):
|
||||
view._cut_buffer = ""
|
||||
|
||||
line_str = buffer.get_text(start_itr, end_itr, True)
|
||||
view._cut_buffer += line_str
|
||||
|
||||
buffer.delete(start_itr, end_itr)
|
||||
|
||||
set_temp_cut_buffer_delayed(view)
|
||||
24
plugins/code/commands/nanoesq_temp_buffer/helpers.py
Normal file
24
plugins/code/commands/nanoesq_temp_buffer/helpers.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
def clear_temp_cut_buffer_delayed(view: any):
|
||||
if not hasattr(view, "_cut_temp_timeout_id"): return
|
||||
if not view._cut_temp_timeout_id: return
|
||||
|
||||
GLib.source_remove(view._cut_temp_timeout_id)
|
||||
|
||||
def set_temp_cut_buffer_delayed(view: any):
|
||||
def clear_temp_buffer(view: any):
|
||||
view._cut_buffer = ""
|
||||
view._cut_temp_timeout_id = None
|
||||
return False
|
||||
|
||||
view._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer, view)
|
||||
7
plugins/code/commands/nanoesq_temp_buffer/manifest.json
Normal file
7
plugins/code/commands/nanoesq_temp_buffer/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Nanoesq Temp Buffer",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
||||
|
||||
|
||||
class Handler2:
|
||||
@staticmethod
|
||||
def execute(
|
||||
view: GtkSource.View,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
logger.debug("Command: Paste Temp Buffer")
|
||||
if not hasattr(view, "_cut_temp_timeout_id"): return
|
||||
if not hasattr(view, "_cut_buffer"): return
|
||||
if not view._cut_buffer: return
|
||||
|
||||
clear_temp_cut_buffer_delayed(view)
|
||||
|
||||
buffer = view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
insert_itr = itr.copy()
|
||||
|
||||
buffer.insert(insert_itr, view._cut_buffer, -1)
|
||||
|
||||
set_temp_cut_buffer_delayed(view)
|
||||
43
plugins/code/commands/nanoesq_temp_buffer/plugin.py
Normal file
43
plugins/code/commands/nanoesq_temp_buffer/plugin.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||
|
||||
from plugins.plugin_types import PluginCode
|
||||
|
||||
from .cut_to_temp_buffer import Handler
|
||||
from .paste_temp_buffer import Handler2
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginCode):
|
||||
def __init__(self):
|
||||
super(Plugin, self).__init__()
|
||||
|
||||
|
||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||
...
|
||||
|
||||
def load(self):
|
||||
event = Event_Factory.create_event("register_command",
|
||||
command_name = "cut_to_temp_buffer",
|
||||
command = Handler,
|
||||
binding_mode = "held",
|
||||
binding = "<Control>k"
|
||||
)
|
||||
|
||||
self.emit_to("source_views", event)
|
||||
|
||||
event = Event_Factory.create_event("register_command",
|
||||
command_name = "paste_temp_buffer",
|
||||
command = Handler2,
|
||||
binding_mode = "held",
|
||||
binding = "<Control>u"
|
||||
)
|
||||
|
||||
self.emit_to("source_views", event)
|
||||
|
||||
def run(self):
|
||||
...
|
||||
@@ -1,159 +0,0 @@
|
||||
# AGENTS.md - LSP Manager Plugin
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Newton editor plugin providing LSP (Language Server Protocol) code completion via WebSocket. Written in Python using GTK3/GtkSource.
|
||||
|
||||
## Build/Lint/Test Commands
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all websocket library tests
|
||||
python -m unittest discover -s libs/websocket/tests
|
||||
|
||||
# Run a single test file
|
||||
python -m unittest libs.websocket.tests.test_websocket
|
||||
|
||||
# Run a single test
|
||||
python -m unittest libs.websocket.tests.test_websocket.WebSocketTest.test_default_timeout
|
||||
```
|
||||
|
||||
### Environment
|
||||
|
||||
- Python 3.x
|
||||
- GTK 3.0 with GtkSource 4
|
||||
- No build system (pyproject.toml/setup.py) - direct execution
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Import Organization
|
||||
|
||||
Always use three-section ordering:
|
||||
```python
|
||||
# Python imports
|
||||
import json
|
||||
from os import path
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('GtkSource', '4')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from libs.event_factory import Event_Factory
|
||||
from plugins.plugin_types import PluginCode
|
||||
from .lsp_manager import LSPManager
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
- 4-space indentation
|
||||
- Line length: ~100 chars (soft limit)
|
||||
- No trailing whitespace
|
||||
- Use f-strings for string formatting: `f"{variable} text"`
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Classes**: PascalCase (`LSPManager`, `ProviderResponseCache`)
|
||||
- **Methods/functions**: snake_case (`create_client`, `load_lsp_servers_config`)
|
||||
- **Private members**: leading underscore (`_setup_styling`, `_init_params`)
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
|
||||
### Type Hints
|
||||
|
||||
- Use Python 3.x type hints
|
||||
- Common types: `str`, `int`, `dict`, `list`, `bool`
|
||||
- For untyped parameters, use `any`:
|
||||
```python
|
||||
def execute(view: any, *args, **kwargs)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use try/except blocks with specific exception types
|
||||
- Use `logger.error()` for logging errors with context
|
||||
- Return early on failure conditions:
|
||||
```python
|
||||
if not lang_id: return
|
||||
if not lang_id in self.servers_config: return
|
||||
```
|
||||
|
||||
### Class Structure
|
||||
|
||||
```python
|
||||
class LSPManager(Gtk.Dialog):
|
||||
def __init__(self):
|
||||
super(LSPManager, self).__init__()
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
```
|
||||
|
||||
### GTK Patterns
|
||||
|
||||
- Use `gi.require_version()` before importing GTK modules
|
||||
- Use `GLib.idle_add()` for deferred UI updates
|
||||
- Connect signals with `widget.connect("signal_name", handler)`
|
||||
|
||||
### Empty Methods
|
||||
|
||||
Use ellipsis for stub/placeholder methods:
|
||||
```python
|
||||
def _subscribe_to_events(self):
|
||||
...
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
- Import `logger` from application (available globally)
|
||||
- Use `logger.debug()`, `logger.error()` for appropriate levels
|
||||
- Include context in error messages:
|
||||
```python
|
||||
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
|
||||
```
|
||||
|
||||
### Inheritance Patterns
|
||||
|
||||
```python
|
||||
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||
__gtype_name__ = 'LSPProvider'
|
||||
|
||||
def do_get_name(self):
|
||||
return "LSP Code Completion"
|
||||
```
|
||||
|
||||
### Testing Patterns (websocket library)
|
||||
|
||||
- Use `unittest.TestCase`
|
||||
- Skip tests with decorators:
|
||||
```python
|
||||
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||
def test_remote(self):
|
||||
...
|
||||
```
|
||||
- Environment variables for test configuration:
|
||||
- `TEST_WITH_INTERNET=1` - enable internet tests
|
||||
- `LOCAL_WS_SERVER_PORT=9999` - enable local server tests
|
||||
|
||||
### File Organization
|
||||
|
||||
- Main plugin: `plugin.py`
|
||||
- Core logic: `lsp_manager.py`, `provider.py`, `provider_response_cache.py`
|
||||
- Controllers: `controllers/` directory
|
||||
- Config: `configs/` directory
|
||||
- Embedded libs: `libs/websocket/`
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- Avoid bare `except:` - use specific exceptions
|
||||
- Avoid global mutable state where possible
|
||||
- Don't commit secrets or credentials
|
||||
@@ -1,38 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
def execute(
|
||||
view: GtkSource.View,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
logger.debug("Command: Cut to Temp Buffer")
|
||||
|
||||
view.clear_temp_cut_buffer_delayed()
|
||||
|
||||
buffer = view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
start_itr = itr.copy()
|
||||
end_itr = itr.copy()
|
||||
start_line = itr.get_line() + 1
|
||||
start_char = itr.get_line_offset()
|
||||
|
||||
start_itr.backward_visible_line()
|
||||
start_itr.forward_line()
|
||||
end_itr.forward_line()
|
||||
|
||||
line_str = buffer.get_slice(start_itr, end_itr, True)
|
||||
view._cut_buffer += f"{line_str}"
|
||||
buffer.delete(start_itr, end_itr)
|
||||
|
||||
view.set_temp_cut_buffer_delayed()
|
||||
@@ -6,6 +6,8 @@ import gi
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GtkSource
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
@@ -21,8 +23,8 @@ def execute(
|
||||
):
|
||||
logger.debug("Command: DnD Load File To Buffer")
|
||||
file = view.command.new_file(view)
|
||||
|
||||
gfile = Gio.File.new_for_uri(uri)
|
||||
|
||||
view.command.exec_with_args(
|
||||
"load_file",
|
||||
view, gfile, file
|
||||
@@ -31,3 +33,4 @@ def execute(
|
||||
view.set_buffer(file.buffer)
|
||||
|
||||
update_info_bar_if_focused(view.command, view)
|
||||
view.emit("focus-in-event", Gdk.Event())
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
def execute(
|
||||
view: GtkSource.View,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
logger.debug("Command: Paste Temp Buffer")
|
||||
|
||||
view.clear_temp_cut_buffer_delayed()
|
||||
|
||||
buffer = view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
insert_itr = itr.copy()
|
||||
|
||||
buffer.insert(insert_itr, view._cut_buffer, -1)
|
||||
|
||||
view.set_temp_cut_buffer_delayed()
|
||||
@@ -109,7 +109,7 @@ class ProviderResponseCacheBase:
|
||||
if not start_iter.starts_word():
|
||||
start_iter.backward_word_start()
|
||||
|
||||
if not end_iter.ends_word():
|
||||
if not end_iter.ends_line() and not end_iter.ends_word():
|
||||
end_iter.forward_word_end()
|
||||
|
||||
buffer = start_iter.get_buffer()
|
||||
|
||||
@@ -43,8 +43,8 @@ class CompletionController(ControllerBase):
|
||||
self.provider_process_file_save(event)
|
||||
elif isinstance(event, Code_Event_Types.TextChangedEvent):
|
||||
self.provider_process_file_change(event)
|
||||
# elif isinstance(event, Code_Event_Types.RequestCompletionEvent):
|
||||
# self.request_unbound_completion( event.view.get_completion() )
|
||||
elif isinstance(event, Code_Event_Types.RequestCompletionEvent):
|
||||
self.request_unbound_completion(event)
|
||||
|
||||
|
||||
def register_completer(self, completer: GtkSource.Completion):
|
||||
@@ -92,8 +92,17 @@ class CompletionController(ControllerBase):
|
||||
for provider in self._providers.values():
|
||||
provider.response_cache.process_file_change(event)
|
||||
|
||||
def request_unbound_completion(self, completer: GtkSource.Completion):
|
||||
def request_unbound_completion(self, event: Code_Event_Types.RequestCompletionEvent):
|
||||
completer = event.view.get_completion()
|
||||
providers = [ *self._providers.values() ]
|
||||
|
||||
if event.provider:
|
||||
if not isinstance(event.provider, list):
|
||||
providers = [ event.provider ]
|
||||
else:
|
||||
providers = event.provider
|
||||
|
||||
completer.start(
|
||||
[ *self._providers.values() ],
|
||||
providers,
|
||||
completer.create_context()
|
||||
)
|
||||
|
||||
@@ -33,7 +33,8 @@ class FilesController(ControllerBase, list):
|
||||
|
||||
|
||||
def filter_loaded(self, event: Code_Event_Types.FilterOutLoadedFilesEvent):
|
||||
loaded_paths = {file.fpath for file in self}
|
||||
loaded_paths = {file.fpath for file in self if not file.fpath == "buffer"}
|
||||
|
||||
files = [
|
||||
uri for uri in event.uris if not any(path in uri for path in loaded_paths)
|
||||
]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from ...source_view import SourceView
|
||||
@@ -21,6 +24,7 @@ class SourceViewSignalMapper:
|
||||
def set_buffer_to_active_view(self, buffer):
|
||||
self.active_view.set_buffer(buffer)
|
||||
self.active_view.command.exec("update_info_bar")
|
||||
GLib.idle_add(self._scroll_to_iter)
|
||||
|
||||
def connect_signals(self, source_view: SourceView):
|
||||
signal_mappings = self._get_signal_mappings()
|
||||
@@ -35,6 +39,12 @@ class SourceViewSignalMapper:
|
||||
def insert_text(self, file, string: str):
|
||||
return self.state_manager.handle_insert_text(self.active_view, file, string)
|
||||
|
||||
def _scroll_to_iter(self):
|
||||
buffer = self.active_view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
|
||||
self.active_view.scroll_to_iter(itr, 0.2, False, 0, 0)
|
||||
|
||||
def _get_signal_mappings(self):
|
||||
return {
|
||||
"focus-in-event": self._focus_in_event,
|
||||
|
||||
@@ -36,7 +36,9 @@ class SourceViewsController(ControllerBase, list):
|
||||
|
||||
if not self.signal_mapper.active_view: return
|
||||
|
||||
if isinstance(event, Code_Event_Types.TextChangedEvent):
|
||||
if isinstance(event, Code_Event_Types.GetActiveViewEvent):
|
||||
event.response = self.signal_mapper.active_view
|
||||
elif isinstance(event, Code_Event_Types.TextChangedEvent):
|
||||
self.signal_mapper.active_view.command.exec("update_info_bar")
|
||||
elif isinstance(event, Code_Event_Types.SetActiveFileEvent):
|
||||
self.signal_mapper.set_buffer_to_active_view(event.buffer)
|
||||
|
||||
@@ -6,7 +6,6 @@ gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
@@ -22,9 +21,6 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
|
||||
|
||||
self.state = state
|
||||
|
||||
self._cut_temp_timeout_id = None
|
||||
self._cut_buffer = ""
|
||||
|
||||
self.sibling_right = None
|
||||
self.sibling_left = None
|
||||
|
||||
@@ -74,15 +70,3 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
|
||||
)
|
||||
|
||||
self._set_up_dnd()
|
||||
|
||||
def clear_temp_cut_buffer_delayed(self):
|
||||
if self._cut_temp_timeout_id:
|
||||
GLib.source_remove(self._cut_temp_timeout_id)
|
||||
|
||||
def set_temp_cut_buffer_delayed(self):
|
||||
def clear_temp_buffer():
|
||||
self._cut_buffer = ""
|
||||
self._cut_temp_timeout_id = None
|
||||
return False
|
||||
|
||||
self._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer)
|
||||
|
||||
@@ -15,18 +15,12 @@
|
||||
"zoom_out": {
|
||||
"held": "<Control>minus"
|
||||
},
|
||||
"cut_to_temp_buffer": {
|
||||
"held": "<Control>k"
|
||||
},
|
||||
"duplicate_line": {
|
||||
"held": "<Control>d"
|
||||
},
|
||||
"go_to": {
|
||||
"released": "<Control>g"
|
||||
},
|
||||
"paste_temp_buffer": {
|
||||
"held": "<Control>u"
|
||||
},
|
||||
"new_file": {
|
||||
"released": "<Control>t"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user