pomera-ai-commander 1.1.1 → 1.2.2

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