pomera-ai-commander 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +680 -0
- package/bin/pomera-ai-commander.js +62 -0
- package/core/__init__.py +66 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/app_context.py +482 -0
- package/core/async_text_processor.py +422 -0
- package/core/backup_manager.py +656 -0
- package/core/backup_recovery_manager.py +1034 -0
- package/core/content_hash_cache.py +509 -0
- package/core/context_menu.py +313 -0
- package/core/data_validator.py +1067 -0
- package/core/database_connection_manager.py +745 -0
- package/core/database_curl_settings_manager.py +609 -0
- package/core/database_promera_ai_settings_manager.py +447 -0
- package/core/database_schema.py +412 -0
- package/core/database_schema_manager.py +396 -0
- package/core/database_settings_manager.py +1508 -0
- package/core/database_settings_manager_interface.py +457 -0
- package/core/dialog_manager.py +735 -0
- package/core/efficient_line_numbers.py +511 -0
- package/core/error_handler.py +747 -0
- package/core/error_service.py +431 -0
- package/core/event_consolidator.py +512 -0
- package/core/mcp/__init__.py +43 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/core/mcp/protocol.py +288 -0
- package/core/mcp/schema.py +251 -0
- package/core/mcp/server_stdio.py +299 -0
- package/core/mcp/tool_registry.py +2345 -0
- package/core/memory_efficient_text_widget.py +712 -0
- package/core/migration_manager.py +915 -0
- package/core/migration_test_suite.py +1086 -0
- package/core/migration_validator.py +1144 -0
- package/core/optimized_find_replace.py +715 -0
- package/core/optimized_pattern_engine.py +424 -0
- package/core/optimized_search_highlighter.py +553 -0
- package/core/performance_monitor.py +675 -0
- package/core/persistence_manager.py +713 -0
- package/core/progressive_stats_calculator.py +632 -0
- package/core/regex_pattern_cache.py +530 -0
- package/core/regex_pattern_library.py +351 -0
- package/core/search_operation_manager.py +435 -0
- package/core/settings_defaults_registry.py +1087 -0
- package/core/settings_integrity_validator.py +1112 -0
- package/core/settings_serializer.py +558 -0
- package/core/settings_validator.py +1824 -0
- package/core/smart_stats_calculator.py +710 -0
- package/core/statistics_update_manager.py +619 -0
- package/core/stats_config_manager.py +858 -0
- package/core/streaming_text_handler.py +723 -0
- package/core/task_scheduler.py +596 -0
- package/core/update_pattern_library.py +169 -0
- package/core/visibility_monitor.py +596 -0
- package/core/widget_cache.py +498 -0
- package/mcp.json +61 -0
- package/package.json +57 -0
- package/pomera.py +7483 -0
- package/pomera_mcp_server.py +144 -0
- package/tools/__init__.py +5 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
- package/tools/ai_tools.py +2892 -0
- package/tools/ascii_art_generator.py +353 -0
- package/tools/base64_tools.py +184 -0
- package/tools/base_tool.py +511 -0
- package/tools/case_tool.py +309 -0
- package/tools/column_tools.py +396 -0
- package/tools/cron_tool.py +885 -0
- package/tools/curl_history.py +601 -0
- package/tools/curl_processor.py +1208 -0
- package/tools/curl_settings.py +503 -0
- package/tools/curl_tool.py +5467 -0
- package/tools/diff_viewer.py +1072 -0
- package/tools/email_extraction_tool.py +249 -0
- package/tools/email_header_analyzer.py +426 -0
- package/tools/extraction_tools.py +250 -0
- package/tools/find_replace.py +1751 -0
- package/tools/folder_file_reporter.py +1463 -0
- package/tools/folder_file_reporter_adapter.py +480 -0
- package/tools/generator_tools.py +1217 -0
- package/tools/hash_generator.py +256 -0
- package/tools/html_tool.py +657 -0
- package/tools/huggingface_helper.py +449 -0
- package/tools/jsonxml_tool.py +730 -0
- package/tools/line_tools.py +419 -0
- package/tools/list_comparator.py +720 -0
- package/tools/markdown_tools.py +562 -0
- package/tools/mcp_widget.py +1417 -0
- package/tools/notes_widget.py +973 -0
- package/tools/number_base_converter.py +373 -0
- package/tools/regex_extractor.py +572 -0
- package/tools/slug_generator.py +311 -0
- package/tools/sorter_tools.py +459 -0
- package/tools/string_escape_tool.py +393 -0
- package/tools/text_statistics_tool.py +366 -0
- package/tools/text_wrapper.py +431 -0
- package/tools/timestamp_converter.py +422 -0
- package/tools/tool_loader.py +710 -0
- package/tools/translator_tools.py +523 -0
- package/tools/url_link_extractor.py +262 -0
- package/tools/url_parser.py +205 -0
- package/tools/whitespace_tools.py +356 -0
- package/tools/word_frequency_counter.py +147 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Efficient line number rendering system for Promera AI Commander.
|
|
3
|
+
Optimizes line number display for large documents by only rendering visible lines
|
|
4
|
+
and implementing intelligent caching and debouncing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import tkinter as tk
|
|
8
|
+
from tkinter import scrolledtext
|
|
9
|
+
import platform
|
|
10
|
+
import time
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Dict, List, Tuple, Optional, Any
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LineInfo:
|
|
17
|
+
"""Information about a line in the text widget."""
|
|
18
|
+
line_number: int
|
|
19
|
+
y_position: int
|
|
20
|
+
height: int
|
|
21
|
+
is_visible: bool
|
|
22
|
+
|
|
23
|
+
class EfficientLineNumbers(tk.Frame):
|
|
24
|
+
"""
|
|
25
|
+
Optimized line number widget that only renders visible lines
|
|
26
|
+
and implements intelligent caching and debouncing.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, *args, **kwargs):
|
|
30
|
+
super().__init__(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
# Configuration
|
|
33
|
+
self.line_number_width = 50 # Adjustable width
|
|
34
|
+
self.debounce_delay = 5 # ms - very responsive updates
|
|
35
|
+
self.cache_size_limit = 1000 # Maximum cached line positions
|
|
36
|
+
|
|
37
|
+
# Create widgets
|
|
38
|
+
self.text = scrolledtext.ScrolledText(
|
|
39
|
+
self, wrap=tk.WORD, height=15, width=50, undo=True
|
|
40
|
+
)
|
|
41
|
+
self.linenumbers = tk.Canvas(
|
|
42
|
+
self, width=self.line_number_width, bg='#f0f0f0',
|
|
43
|
+
highlightthickness=0, bd=0
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Layout
|
|
47
|
+
self.linenumbers.pack(side=tk.LEFT, fill=tk.Y)
|
|
48
|
+
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
49
|
+
|
|
50
|
+
# Performance tracking
|
|
51
|
+
self.visible_lines_cache: Dict[str, LineInfo] = {}
|
|
52
|
+
self.last_scroll_position: Optional[Tuple[float, float]] = None
|
|
53
|
+
self.last_content_hash: Optional[str] = None
|
|
54
|
+
self.last_update_time: float = 0
|
|
55
|
+
self.pending_update_id: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
# Rendering optimization
|
|
58
|
+
self.canvas_items: List[int] = [] # Track canvas items for efficient clearing
|
|
59
|
+
self.font_metrics: Optional[Dict[str, int]] = None
|
|
60
|
+
self.line_height: int = 16 # Default, will be calculated
|
|
61
|
+
|
|
62
|
+
# Setup event bindings
|
|
63
|
+
self._setup_event_bindings()
|
|
64
|
+
|
|
65
|
+
# Ensure scrollbar is properly connected
|
|
66
|
+
self._setup_scrollbar_sync()
|
|
67
|
+
|
|
68
|
+
# Initial render
|
|
69
|
+
self.after(10, self._update_line_numbers)
|
|
70
|
+
|
|
71
|
+
def _setup_event_bindings(self):
|
|
72
|
+
"""Setup optimized event bindings."""
|
|
73
|
+
# Mouse wheel events on both text and line numbers
|
|
74
|
+
self.linenumbers.bind("<MouseWheel>", self._on_mousewheel)
|
|
75
|
+
self.linenumbers.bind("<Button-4>", self._on_mousewheel)
|
|
76
|
+
self.linenumbers.bind("<Button-5>", self._on_mousewheel)
|
|
77
|
+
self.text.bind("<MouseWheel>", self._on_text_mousewheel)
|
|
78
|
+
self.text.bind("<Button-4>", self._on_text_mousewheel)
|
|
79
|
+
self.text.bind("<Button-5>", self._on_text_mousewheel)
|
|
80
|
+
|
|
81
|
+
# Key events for navigation (works even when disabled)
|
|
82
|
+
self.text.bind("<Up>", self._on_navigation_key)
|
|
83
|
+
self.text.bind("<Down>", self._on_navigation_key)
|
|
84
|
+
self.text.bind("<Page_Up>", self._on_navigation_key)
|
|
85
|
+
self.text.bind("<Page_Down>", self._on_navigation_key)
|
|
86
|
+
self.text.bind("<Home>", self._on_navigation_key)
|
|
87
|
+
self.text.bind("<End>", self._on_navigation_key)
|
|
88
|
+
|
|
89
|
+
# Text modification events (with immediate and debounced updates)
|
|
90
|
+
self.text.bind("<<Modified>>", self._on_text_modified_debounced)
|
|
91
|
+
self.text.bind("<Configure>", self._on_configure_debounced)
|
|
92
|
+
self.text.bind("<KeyPress>", self._on_key_press)
|
|
93
|
+
self.text.bind("<KeyRelease>", self._on_key_release)
|
|
94
|
+
self.text.bind("<Button-1>", self._on_mouse_click)
|
|
95
|
+
|
|
96
|
+
# Focus events for optimization
|
|
97
|
+
self.text.bind("<FocusIn>", self._on_focus_in)
|
|
98
|
+
self.text.bind("<FocusOut>", self._on_focus_out)
|
|
99
|
+
|
|
100
|
+
def _setup_scrollbar_sync(self):
|
|
101
|
+
"""Setup proper scrollbar synchronization."""
|
|
102
|
+
# Configure the scrollbar to call our custom scroll handler
|
|
103
|
+
original_yscrollcommand = self.text.cget('yscrollcommand')
|
|
104
|
+
|
|
105
|
+
def combined_scroll_command(*args):
|
|
106
|
+
# Call original scroll command (updates scrollbar)
|
|
107
|
+
if original_yscrollcommand:
|
|
108
|
+
self.text.tk.call(original_yscrollcommand, *args)
|
|
109
|
+
# Update line numbers immediately for scrollbar changes
|
|
110
|
+
self._update_line_numbers()
|
|
111
|
+
|
|
112
|
+
# Set up the text widget to call our combined handler
|
|
113
|
+
self.text.config(yscrollcommand=combined_scroll_command)
|
|
114
|
+
|
|
115
|
+
# Configure scrollbar to call our scroll handler
|
|
116
|
+
self.text.vbar.config(command=self._on_text_scroll)
|
|
117
|
+
|
|
118
|
+
# Also bind scrollbar events directly
|
|
119
|
+
self.text.vbar.bind("<Button-1>", lambda e: self.after(10, self._update_line_numbers))
|
|
120
|
+
self.text.vbar.bind("<B1-Motion>", lambda e: self.after(1, self._update_line_numbers))
|
|
121
|
+
|
|
122
|
+
def _on_text_scroll(self, *args):
|
|
123
|
+
"""Handle scrolling with optimized line number updates."""
|
|
124
|
+
# Apply the scroll to the text widget
|
|
125
|
+
self.text.yview(*args)
|
|
126
|
+
|
|
127
|
+
# Immediate line number update for scrolling (no delay)
|
|
128
|
+
self._update_line_numbers()
|
|
129
|
+
|
|
130
|
+
def _on_mousewheel(self, event):
|
|
131
|
+
"""Handle mouse wheel scrolling with platform-specific logic."""
|
|
132
|
+
if platform.system() == "Windows":
|
|
133
|
+
delta = int(-1 * (event.delta / 120))
|
|
134
|
+
elif platform.system() == "Darwin": # macOS
|
|
135
|
+
delta = int(-1 * event.delta)
|
|
136
|
+
else: # Linux
|
|
137
|
+
delta = -1 if event.num == 4 else 1
|
|
138
|
+
|
|
139
|
+
# Scroll the text widget
|
|
140
|
+
self.text.yview_scroll(delta, "units")
|
|
141
|
+
|
|
142
|
+
# Immediate line number update for mouse wheel scrolling (no delay)
|
|
143
|
+
self._update_line_numbers()
|
|
144
|
+
return "break"
|
|
145
|
+
|
|
146
|
+
def _on_text_modified_debounced(self, event=None):
|
|
147
|
+
"""Handle text modifications with debouncing."""
|
|
148
|
+
if event and hasattr(event.widget, 'edit_modified') and event.widget.edit_modified():
|
|
149
|
+
event.widget.edit_modified(False)
|
|
150
|
+
self._schedule_line_number_update()
|
|
151
|
+
|
|
152
|
+
def _on_configure_debounced(self, event=None):
|
|
153
|
+
"""Handle widget configuration changes with debouncing."""
|
|
154
|
+
self._schedule_line_number_update()
|
|
155
|
+
|
|
156
|
+
def _on_focus_in(self, event=None):
|
|
157
|
+
"""Handle focus in - ensure line numbers are up to date."""
|
|
158
|
+
self._schedule_line_number_update()
|
|
159
|
+
|
|
160
|
+
def _on_focus_out(self, event=None):
|
|
161
|
+
"""Handle focus out - can reduce update frequency."""
|
|
162
|
+
pass # Could implement reduced update frequency when not focused
|
|
163
|
+
|
|
164
|
+
def _on_key_press(self, event=None):
|
|
165
|
+
"""Handle key press events - immediate update for Enter key."""
|
|
166
|
+
if event and event.keysym in ['Return', 'BackSpace', 'Delete']:
|
|
167
|
+
# For line-changing operations, update immediately
|
|
168
|
+
self.after_idle(self._update_line_numbers)
|
|
169
|
+
else:
|
|
170
|
+
# For other keys, use debounced update
|
|
171
|
+
self._schedule_line_number_update()
|
|
172
|
+
|
|
173
|
+
def _on_key_release(self, event=None):
|
|
174
|
+
"""Handle key release events."""
|
|
175
|
+
# Schedule update after key release
|
|
176
|
+
self._schedule_line_number_update()
|
|
177
|
+
|
|
178
|
+
def _on_mouse_click(self, event=None):
|
|
179
|
+
"""Handle mouse click events."""
|
|
180
|
+
# Update line numbers after mouse click (cursor position change)
|
|
181
|
+
self.after_idle(self._update_line_numbers)
|
|
182
|
+
|
|
183
|
+
def _on_text_mousewheel(self, event):
|
|
184
|
+
"""Handle mouse wheel scrolling on text widget."""
|
|
185
|
+
# Let the text widget handle the scroll normally, then update line numbers immediately
|
|
186
|
+
self.after(1, self._update_line_numbers) # Very short delay to let text widget scroll first
|
|
187
|
+
# Don't return "break" so the text widget can handle the scroll
|
|
188
|
+
|
|
189
|
+
def _on_navigation_key(self, event):
|
|
190
|
+
"""Handle navigation keys that might change the view."""
|
|
191
|
+
# Let the text widget handle the key first, then update line numbers
|
|
192
|
+
self.after(1, self._update_line_numbers)
|
|
193
|
+
# Don't return "break" so the text widget can handle the key
|
|
194
|
+
|
|
195
|
+
def _schedule_line_number_update(self):
|
|
196
|
+
"""Schedule a debounced line number update."""
|
|
197
|
+
current_time = time.time() * 1000 # Convert to milliseconds
|
|
198
|
+
|
|
199
|
+
# Cancel pending update
|
|
200
|
+
if self.pending_update_id:
|
|
201
|
+
self.after_cancel(self.pending_update_id)
|
|
202
|
+
|
|
203
|
+
# Schedule new update
|
|
204
|
+
self.pending_update_id = self.after(
|
|
205
|
+
self.debounce_delay,
|
|
206
|
+
self._update_line_numbers
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self.last_update_time = current_time
|
|
210
|
+
|
|
211
|
+
def _update_line_numbers(self):
|
|
212
|
+
"""Update line numbers with optimizations."""
|
|
213
|
+
try:
|
|
214
|
+
self.pending_update_id = None
|
|
215
|
+
|
|
216
|
+
# Get current view information
|
|
217
|
+
view_info = self._get_view_info()
|
|
218
|
+
if not view_info:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Check if update is actually needed
|
|
222
|
+
if not self._needs_update(view_info):
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Clear existing canvas items efficiently
|
|
226
|
+
self._clear_canvas_items()
|
|
227
|
+
|
|
228
|
+
# Get visible lines only
|
|
229
|
+
visible_lines = self._get_visible_lines()
|
|
230
|
+
|
|
231
|
+
# Render visible line numbers
|
|
232
|
+
self._render_line_numbers(visible_lines)
|
|
233
|
+
|
|
234
|
+
# Update cache
|
|
235
|
+
self._update_cache(view_info, visible_lines)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
# Graceful error handling
|
|
239
|
+
print(f"Error updating line numbers: {e}")
|
|
240
|
+
|
|
241
|
+
def _get_view_info(self) -> Optional[Dict[str, Any]]:
|
|
242
|
+
"""Get current view information."""
|
|
243
|
+
try:
|
|
244
|
+
return {
|
|
245
|
+
'scroll_position': self.text.yview(),
|
|
246
|
+
'widget_height': self.text.winfo_height(),
|
|
247
|
+
'content_hash': self._get_content_hash()
|
|
248
|
+
}
|
|
249
|
+
except Exception:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def _get_content_hash(self) -> str:
|
|
253
|
+
"""Get a hash of the current content for change detection."""
|
|
254
|
+
try:
|
|
255
|
+
content = self.text.get("1.0", "end-1c")
|
|
256
|
+
# Simple hash based on content length and first/last chars
|
|
257
|
+
if content:
|
|
258
|
+
return f"{len(content)}_{content[:10]}_{content[-10:]}"
|
|
259
|
+
return "empty"
|
|
260
|
+
except Exception:
|
|
261
|
+
return "error"
|
|
262
|
+
|
|
263
|
+
def _needs_update(self, view_info: Dict[str, Any]) -> bool:
|
|
264
|
+
"""Check if line numbers actually need updating."""
|
|
265
|
+
# Always update if no previous state
|
|
266
|
+
if self.last_scroll_position is None or self.last_content_hash is None:
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# Check if content changed
|
|
270
|
+
if view_info['content_hash'] != self.last_content_hash:
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
# Check if scroll position changed significantly
|
|
274
|
+
old_top, old_bottom = self.last_scroll_position
|
|
275
|
+
new_top, new_bottom = view_info['scroll_position']
|
|
276
|
+
|
|
277
|
+
# Update if scroll position changed by more than 0.1% of view (more sensitive)
|
|
278
|
+
scroll_threshold = 0.001
|
|
279
|
+
if (abs(new_top - old_top) > scroll_threshold or
|
|
280
|
+
abs(new_bottom - old_bottom) > scroll_threshold):
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def _clear_canvas_items(self):
|
|
286
|
+
"""Efficiently clear canvas items."""
|
|
287
|
+
if self.canvas_items:
|
|
288
|
+
for item_id in self.canvas_items:
|
|
289
|
+
try:
|
|
290
|
+
self.linenumbers.delete(item_id)
|
|
291
|
+
except Exception:
|
|
292
|
+
pass # Item may already be deleted
|
|
293
|
+
self.canvas_items.clear()
|
|
294
|
+
else:
|
|
295
|
+
# Fallback to delete all
|
|
296
|
+
self.linenumbers.delete("all")
|
|
297
|
+
|
|
298
|
+
def _get_visible_lines(self) -> List[LineInfo]:
|
|
299
|
+
"""Get information about currently visible lines."""
|
|
300
|
+
visible_lines = []
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Get the first visible line
|
|
304
|
+
first_visible = self.text.index("@0,0")
|
|
305
|
+
|
|
306
|
+
# Calculate font metrics if not cached
|
|
307
|
+
if self.font_metrics is None:
|
|
308
|
+
self._calculate_font_metrics()
|
|
309
|
+
|
|
310
|
+
# Iterate through visible lines
|
|
311
|
+
current_index = first_visible
|
|
312
|
+
y_offset = 0
|
|
313
|
+
|
|
314
|
+
while True:
|
|
315
|
+
try:
|
|
316
|
+
# Get line display info
|
|
317
|
+
dline_info = self.text.dlineinfo(current_index)
|
|
318
|
+
if dline_info is None:
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
# Extract line information
|
|
322
|
+
x, y, width, height, baseline = dline_info
|
|
323
|
+
line_number = int(current_index.split('.')[0])
|
|
324
|
+
|
|
325
|
+
# Check if line is within visible area
|
|
326
|
+
widget_height = self.text.winfo_height()
|
|
327
|
+
if y > widget_height:
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
visible_lines.append(LineInfo(
|
|
331
|
+
line_number=line_number,
|
|
332
|
+
y_position=y,
|
|
333
|
+
height=height,
|
|
334
|
+
is_visible=True
|
|
335
|
+
))
|
|
336
|
+
|
|
337
|
+
# Move to next line
|
|
338
|
+
next_index = self.text.index(f"{current_index}+1line")
|
|
339
|
+
if next_index == current_index:
|
|
340
|
+
break
|
|
341
|
+
current_index = next_index
|
|
342
|
+
|
|
343
|
+
except Exception:
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print(f"Error getting visible lines: {e}")
|
|
348
|
+
|
|
349
|
+
return visible_lines
|
|
350
|
+
|
|
351
|
+
def _calculate_font_metrics(self):
|
|
352
|
+
"""Calculate font metrics for line height estimation."""
|
|
353
|
+
try:
|
|
354
|
+
# Create a temporary text item to measure font
|
|
355
|
+
temp_item = self.linenumbers.create_text(
|
|
356
|
+
0, 0, text="1", font=("TkDefaultFont",), anchor="nw"
|
|
357
|
+
)
|
|
358
|
+
bbox = self.linenumbers.bbox(temp_item)
|
|
359
|
+
if bbox:
|
|
360
|
+
self.line_height = bbox[3] - bbox[1]
|
|
361
|
+
self.linenumbers.delete(temp_item)
|
|
362
|
+
|
|
363
|
+
self.font_metrics = {
|
|
364
|
+
'line_height': self.line_height,
|
|
365
|
+
'char_width': 8 # Approximate
|
|
366
|
+
}
|
|
367
|
+
except Exception:
|
|
368
|
+
self.font_metrics = {
|
|
369
|
+
'line_height': 16,
|
|
370
|
+
'char_width': 8
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
def _render_line_numbers(self, visible_lines: List[LineInfo]):
|
|
374
|
+
"""Render line numbers for visible lines only."""
|
|
375
|
+
if not visible_lines:
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
# Calculate optimal text positioning
|
|
380
|
+
text_x = self.line_number_width - 5 # Right-aligned with padding
|
|
381
|
+
|
|
382
|
+
# Render each visible line number
|
|
383
|
+
for line_info in visible_lines:
|
|
384
|
+
try:
|
|
385
|
+
item_id = self.linenumbers.create_text(
|
|
386
|
+
text_x, line_info.y_position,
|
|
387
|
+
text=str(line_info.line_number),
|
|
388
|
+
anchor="ne", # Right-aligned
|
|
389
|
+
fill="gray",
|
|
390
|
+
font=("TkDefaultFont", "9")
|
|
391
|
+
)
|
|
392
|
+
self.canvas_items.append(item_id)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
print(f"Error rendering line {line_info.line_number}: {e}")
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
# Sync canvas scroll position with text widget
|
|
398
|
+
self._sync_canvas_scroll()
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
print(f"Error rendering line numbers: {e}")
|
|
402
|
+
|
|
403
|
+
def _sync_canvas_scroll(self):
|
|
404
|
+
"""Synchronize canvas scroll position with text widget."""
|
|
405
|
+
try:
|
|
406
|
+
# Get the text widget's current scroll position
|
|
407
|
+
scroll_top, scroll_bottom = self.text.yview()
|
|
408
|
+
|
|
409
|
+
# Move the canvas to match the text widget's scroll position
|
|
410
|
+
self.linenumbers.yview_moveto(scroll_top)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
print(f"Error syncing canvas scroll: {e}")
|
|
413
|
+
|
|
414
|
+
def _update_cache(self, view_info: Dict[str, Any], visible_lines: List[LineInfo]):
|
|
415
|
+
"""Update internal cache with current state."""
|
|
416
|
+
self.last_scroll_position = view_info['scroll_position']
|
|
417
|
+
self.last_content_hash = view_info['content_hash']
|
|
418
|
+
|
|
419
|
+
# Update visible lines cache (with size limit)
|
|
420
|
+
cache_key = f"{view_info['content_hash']}_{view_info['scroll_position']}"
|
|
421
|
+
self.visible_lines_cache[cache_key] = visible_lines
|
|
422
|
+
|
|
423
|
+
# Limit cache size
|
|
424
|
+
if len(self.visible_lines_cache) > self.cache_size_limit:
|
|
425
|
+
# Remove oldest entries (simple FIFO)
|
|
426
|
+
oldest_keys = list(self.visible_lines_cache.keys())[:-self.cache_size_limit//2]
|
|
427
|
+
for key in oldest_keys:
|
|
428
|
+
self.visible_lines_cache.pop(key, None)
|
|
429
|
+
|
|
430
|
+
def get_performance_stats(self) -> Dict[str, Any]:
|
|
431
|
+
"""Get performance statistics for monitoring."""
|
|
432
|
+
return {
|
|
433
|
+
'cache_size': len(self.visible_lines_cache),
|
|
434
|
+
'canvas_items': len(self.canvas_items),
|
|
435
|
+
'last_update_time': self.last_update_time,
|
|
436
|
+
'debounce_delay': self.debounce_delay,
|
|
437
|
+
'line_height': self.line_height
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def clear_cache(self):
|
|
441
|
+
"""Clear internal caches for memory management."""
|
|
442
|
+
self.visible_lines_cache.clear()
|
|
443
|
+
self.last_scroll_position = None
|
|
444
|
+
self.last_content_hash = None
|
|
445
|
+
|
|
446
|
+
def set_line_number_width(self, width: int):
|
|
447
|
+
"""Dynamically adjust line number width."""
|
|
448
|
+
self.line_number_width = width
|
|
449
|
+
self.linenumbers.config(width=width)
|
|
450
|
+
self._schedule_line_number_update()
|
|
451
|
+
|
|
452
|
+
def set_debounce_delay(self, delay: int):
|
|
453
|
+
"""Adjust debounce delay for different performance needs."""
|
|
454
|
+
self.debounce_delay = max(10, min(500, delay)) # Clamp between 10-500ms
|
|
455
|
+
|
|
456
|
+
class OptimizedTextWithLineNumbers(EfficientLineNumbers):
|
|
457
|
+
"""
|
|
458
|
+
Drop-in replacement for the original TextWithLineNumbers class
|
|
459
|
+
with all the performance optimizations including lazy updates.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
def __init__(self, *args, **kwargs):
|
|
463
|
+
super().__init__(*args, **kwargs)
|
|
464
|
+
|
|
465
|
+
# Add performance monitoring integration if available
|
|
466
|
+
try:
|
|
467
|
+
from performance_monitor import get_performance_monitor, PerformanceContext
|
|
468
|
+
self.performance_monitor = get_performance_monitor()
|
|
469
|
+
self.performance_monitoring = True
|
|
470
|
+
except ImportError:
|
|
471
|
+
self.performance_monitor = None
|
|
472
|
+
self.performance_monitoring = False
|
|
473
|
+
|
|
474
|
+
def _on_text_modified(self, event=None):
|
|
475
|
+
"""Compatibility method for the main application."""
|
|
476
|
+
# This method is called by the main application for compatibility
|
|
477
|
+
# Delegate to our optimized update method
|
|
478
|
+
self._schedule_line_number_update()
|
|
479
|
+
|
|
480
|
+
# Handle the modified flag like the original implementation
|
|
481
|
+
if event and hasattr(event.widget, 'edit_modified') and event.widget.edit_modified():
|
|
482
|
+
event.widget.edit_modified(False)
|
|
483
|
+
|
|
484
|
+
def _update_line_numbers(self):
|
|
485
|
+
"""Override with performance monitoring."""
|
|
486
|
+
if self.performance_monitoring and self.performance_monitor:
|
|
487
|
+
try:
|
|
488
|
+
from performance_monitor import PerformanceContext
|
|
489
|
+
with PerformanceContext(self.performance_monitor, 'line_numbers_update'):
|
|
490
|
+
super()._update_line_numbers()
|
|
491
|
+
except ImportError:
|
|
492
|
+
# Fall back to non-monitored update
|
|
493
|
+
super()._update_line_numbers()
|
|
494
|
+
else:
|
|
495
|
+
super()._update_line_numbers()
|
|
496
|
+
|
|
497
|
+
def force_immediate_update(self):
|
|
498
|
+
"""Force an immediate line number update."""
|
|
499
|
+
self._update_line_numbers()
|
|
500
|
+
|
|
501
|
+
def cleanup_resources(self):
|
|
502
|
+
"""Clean up resources when widget is destroyed."""
|
|
503
|
+
# Clear caches
|
|
504
|
+
self.clear_cache()
|
|
505
|
+
|
|
506
|
+
def get_performance_info(self) -> Dict[str, Any]:
|
|
507
|
+
"""Get comprehensive performance information."""
|
|
508
|
+
return self.get_performance_stats()
|
|
509
|
+
|
|
510
|
+
# Backward compatibility alias
|
|
511
|
+
TextWithLineNumbers = OptimizedTextWithLineNumbers
|