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,553 +1,553 @@
1
- #!/usr/bin/env python3
2
- """
3
- Optimized search and highlighting system for text widgets.
4
- Implements progressive highlighting, batching, and non-blocking operations.
5
- """
6
-
7
- import re
8
- import time
9
- import threading
10
- import tkinter as tk
11
- from typing import Dict, List, Optional, Any, Tuple, Generator, Callable
12
- from dataclasses import dataclass, field
13
- from collections import deque
14
- from enum import Enum
15
- import queue
16
-
17
- class HighlightMode(Enum):
18
- """Different highlighting modes for optimization."""
19
- IMMEDIATE = "immediate" # Highlight all matches immediately
20
- PROGRESSIVE = "progressive" # Highlight matches progressively
21
- BATCH = "batch" # Highlight in batches
22
- LAZY = "lazy" # Highlight only visible area
23
-
24
- class SearchState(Enum):
25
- """Search operation states."""
26
- IDLE = "idle"
27
- SEARCHING = "searching"
28
- HIGHLIGHTING = "highlighting"
29
- COMPLETED = "completed"
30
- CANCELLED = "cancelled"
31
- ERROR = "error"
32
-
33
- @dataclass
34
- class HighlightMatch:
35
- """Represents a single highlight match."""
36
- start: int
37
- end: int
38
- text: str
39
- tag_name: str
40
- priority: int = 0
41
-
42
- @property
43
- def length(self) -> int:
44
- return self.end - self.start
45
-
46
- @dataclass
47
- class SearchProgress:
48
- """Progress information for search operations."""
49
- total_chars: int = 0
50
- processed_chars: int = 0
51
- matches_found: int = 0
52
- batches_completed: int = 0
53
- time_elapsed: float = 0.0
54
- estimated_remaining: float = 0.0
55
-
56
- @property
57
- def progress_percent(self) -> float:
58
- if self.total_chars == 0:
59
- return 0.0
60
- return (self.processed_chars / self.total_chars) * 100
61
-
62
- @dataclass
63
- class SearchOperation:
64
- """Represents a search operation with its parameters."""
65
- operation_id: str
66
- pattern: str
67
- text_widget: tk.Text
68
- tag_name: str
69
- flags: int = 0
70
- mode: HighlightMode = HighlightMode.PROGRESSIVE
71
- batch_size: int = 100
72
- max_matches: int = 10000
73
- timeout_ms: int = 5000
74
-
75
- # State
76
- state: SearchState = SearchState.IDLE
77
- matches: List[HighlightMatch] = field(default_factory=list)
78
- progress: SearchProgress = field(default_factory=SearchProgress)
79
- start_time: float = field(default_factory=time.time)
80
-
81
- # Callbacks
82
- progress_callback: Optional[Callable] = None
83
- completion_callback: Optional[Callable] = None
84
- error_callback: Optional[Callable] = None
85
-
86
- class OptimizedSearchHighlighter:
87
- """
88
- High-performance search and highlighting system with progressive updates,
89
- batching, and non-blocking operations for large text documents.
90
- """
91
-
92
- def __init__(self,
93
- default_batch_size: int = 100,
94
- max_concurrent_operations: int = 3,
95
- highlight_timeout_ms: int = 5000):
96
-
97
- self.default_batch_size = default_batch_size
98
- self.max_concurrent_operations = max_concurrent_operations
99
- self.highlight_timeout_ms = highlight_timeout_ms
100
-
101
- # Operation management
102
- self.active_operations: Dict[str, SearchOperation] = {}
103
- self.operation_queue = queue.Queue()
104
- self.operation_lock = threading.RLock()
105
-
106
- # Performance tracking
107
- self.performance_stats = {
108
- 'total_operations': 0,
109
- 'completed_operations': 0,
110
- 'cancelled_operations': 0,
111
- 'error_operations': 0,
112
- 'total_matches_found': 0,
113
- 'total_processing_time': 0.0,
114
- 'average_processing_time': 0.0
115
- }
116
-
117
- # Tag configuration
118
- self.tag_configs = {
119
- 'search_highlight': {'background': 'yellow', 'foreground': 'black'},
120
- 'replace_highlight': {'background': 'pink', 'foreground': 'black'},
121
- 'current_match': {'background': 'orange', 'foreground': 'black'},
122
- 'error_highlight': {'background': 'red', 'foreground': 'white'},
123
- 'yellow_highlight': {'background': 'yellow', 'foreground': 'black'},
124
- 'pink_highlight': {'background': 'pink', 'foreground': 'black'}
125
- }
126
-
127
- # Worker thread for background processing
128
- self.worker_thread = None
129
- self.shutdown_event = threading.Event()
130
- self._start_worker_thread()
131
-
132
- def _start_worker_thread(self):
133
- """Start the background worker thread for processing operations."""
134
- if self.worker_thread is None or not self.worker_thread.is_alive():
135
- self.worker_thread = threading.Thread(
136
- target=self._worker_loop,
137
- daemon=True,
138
- name="SearchHighlighter-Worker"
139
- )
140
- self.worker_thread.start()
141
-
142
- def _worker_loop(self):
143
- """Main worker loop for processing search operations."""
144
- while not self.shutdown_event.is_set():
145
- try:
146
- # Get next operation from queue (with timeout)
147
- operation = self.operation_queue.get(timeout=1.0)
148
- if operation is None: # Shutdown signal
149
- break
150
-
151
- self._process_operation(operation)
152
-
153
- except queue.Empty:
154
- continue
155
- except Exception as e:
156
- print(f"Error in search worker thread: {e}")
157
-
158
- def search_and_highlight(self,
159
- text_widget: tk.Text,
160
- pattern: str,
161
- tag_name: str = 'search_highlight',
162
- mode: HighlightMode = HighlightMode.PROGRESSIVE,
163
- flags: int = 0,
164
- batch_size: Optional[int] = None,
165
- max_matches: int = 10000,
166
- progress_callback: Optional[Callable] = None,
167
- completion_callback: Optional[Callable] = None) -> str:
168
- """
169
- Start a search and highlight operation.
170
-
171
- Args:
172
- text_widget: The tkinter Text widget to search in
173
- pattern: Regular expression pattern to search for
174
- tag_name: Tag name for highlighting matches
175
- mode: Highlighting mode (immediate, progressive, batch, lazy)
176
- flags: Regular expression flags
177
- batch_size: Number of matches to process per batch
178
- max_matches: Maximum number of matches to find
179
- progress_callback: Callback for progress updates
180
- completion_callback: Callback when operation completes
181
-
182
- Returns:
183
- Operation ID for tracking the operation
184
- """
185
- # Generate unique operation ID
186
- operation_id = f"search_{int(time.time() * 1000000)}"
187
-
188
- # Create search operation
189
- operation = SearchOperation(
190
- operation_id=operation_id,
191
- pattern=pattern,
192
- text_widget=text_widget,
193
- tag_name=tag_name,
194
- flags=flags,
195
- mode=mode,
196
- batch_size=batch_size or self.default_batch_size,
197
- max_matches=max_matches,
198
- timeout_ms=self.highlight_timeout_ms,
199
- progress_callback=progress_callback,
200
- completion_callback=completion_callback
201
- )
202
-
203
- # Configure tag if not already configured
204
- self._configure_tag(text_widget, tag_name)
205
-
206
- # Clear existing highlights for this tag
207
- self.clear_highlights(text_widget, tag_name)
208
-
209
- # Add to active operations
210
- with self.operation_lock:
211
- self.active_operations[operation_id] = operation
212
- self.performance_stats['total_operations'] += 1
213
-
214
- # Queue for processing
215
- self.operation_queue.put(operation)
216
-
217
- return operation_id
218
-
219
- def _configure_tag(self, text_widget: tk.Text, tag_name: str):
220
- """Configure highlighting tag in the text widget."""
221
- if tag_name in self.tag_configs:
222
- config = self.tag_configs[tag_name]
223
- text_widget.tag_configure(tag_name, **config)
224
- else:
225
- # Default configuration
226
- text_widget.tag_configure(tag_name, background='yellow', foreground='black')
227
-
228
- def _process_operation(self, operation: SearchOperation):
229
- """Process a search operation in the background."""
230
- try:
231
- operation.state = SearchState.SEARCHING
232
- operation.start_time = time.time()
233
-
234
- # Get text content
235
- content = operation.text_widget.get("1.0", tk.END)
236
- operation.progress.total_chars = len(content)
237
-
238
- # Compile regex pattern
239
- try:
240
- compiled_pattern = re.compile(operation.pattern, operation.flags)
241
- except re.error as e:
242
- operation.state = SearchState.ERROR
243
- if operation.error_callback:
244
- operation.error_callback(operation, str(e))
245
- return
246
-
247
- # Find matches based on mode
248
- if operation.mode == HighlightMode.IMMEDIATE:
249
- self._find_all_matches_immediate(operation, compiled_pattern, content)
250
- elif operation.mode == HighlightMode.PROGRESSIVE:
251
- self._find_matches_progressive(operation, compiled_pattern, content)
252
- elif operation.mode == HighlightMode.BATCH:
253
- self._find_matches_batch(operation, compiled_pattern, content)
254
- elif operation.mode == HighlightMode.LAZY:
255
- self._find_matches_lazy(operation, compiled_pattern, content)
256
-
257
- # Update performance stats
258
- operation.progress.time_elapsed = time.time() - operation.start_time
259
-
260
- with self.operation_lock:
261
- if operation.state != SearchState.CANCELLED:
262
- operation.state = SearchState.COMPLETED
263
- self.performance_stats['completed_operations'] += 1
264
- self.performance_stats['total_matches_found'] += len(operation.matches)
265
- self.performance_stats['total_processing_time'] += operation.progress.time_elapsed
266
-
267
- # Update average processing time
268
- if self.performance_stats['completed_operations'] > 0:
269
- self.performance_stats['average_processing_time'] = (
270
- self.performance_stats['total_processing_time'] /
271
- self.performance_stats['completed_operations']
272
- )
273
-
274
- # Remove from active operations
275
- self.active_operations.pop(operation.operation_id, None)
276
-
277
- # Call completion callback
278
- if operation.completion_callback and operation.state == SearchState.COMPLETED:
279
- operation.completion_callback(operation)
280
-
281
- except Exception as e:
282
- operation.state = SearchState.ERROR
283
- with self.operation_lock:
284
- self.performance_stats['error_operations'] += 1
285
- self.active_operations.pop(operation.operation_id, None)
286
-
287
- if operation.error_callback:
288
- operation.error_callback(operation, str(e))
289
-
290
- def _find_all_matches_immediate(self, operation: SearchOperation, pattern: re.Pattern, content: str):
291
- """Find all matches immediately and highlight them."""
292
- matches = []
293
-
294
- for match in pattern.finditer(content):
295
- if len(matches) >= operation.max_matches:
296
- break
297
-
298
- highlight_match = HighlightMatch(
299
- start=match.start(),
300
- end=match.end(),
301
- text=match.group(),
302
- tag_name=operation.tag_name
303
- )
304
- matches.append(highlight_match)
305
-
306
- operation.matches = matches
307
- operation.progress.matches_found = len(matches)
308
- operation.progress.processed_chars = len(content)
309
-
310
- # Apply highlights immediately
311
- self._apply_highlights_immediate(operation)
312
-
313
- def _find_matches_progressive(self, operation: SearchOperation, pattern: re.Pattern, content: str):
314
- """Find matches progressively with periodic UI updates."""
315
- matches = []
316
- batch_matches = []
317
- last_update_time = time.time()
318
- update_interval = 0.1 # Update UI every 100ms
319
-
320
- for match in pattern.finditer(content):
321
- if operation.state == SearchState.CANCELLED:
322
- break
323
-
324
- if len(matches) >= operation.max_matches:
325
- break
326
-
327
- highlight_match = HighlightMatch(
328
- start=match.start(),
329
- end=match.end(),
330
- text=match.group(),
331
- tag_name=operation.tag_name
332
- )
333
-
334
- matches.append(highlight_match)
335
- batch_matches.append(highlight_match)
336
-
337
- # Update progress
338
- operation.progress.matches_found = len(matches)
339
- operation.progress.processed_chars = match.end()
340
-
341
- # Apply highlights in batches
342
- if (len(batch_matches) >= operation.batch_size or
343
- time.time() - last_update_time > update_interval):
344
-
345
- self._apply_highlights_batch(operation, batch_matches)
346
- batch_matches = []
347
- last_update_time = time.time()
348
-
349
- # Call progress callback
350
- if operation.progress_callback:
351
- operation.progress_callback(operation)
352
-
353
- # Apply remaining highlights
354
- if batch_matches:
355
- self._apply_highlights_batch(operation, batch_matches)
356
-
357
- operation.matches = matches
358
- operation.progress.processed_chars = len(content)
359
-
360
- def _find_matches_batch(self, operation: SearchOperation, pattern: re.Pattern, content: str):
361
- """Find matches in batches with controlled processing."""
362
- matches = []
363
- chunk_size = 10000 # Process 10KB chunks
364
-
365
- for i in range(0, len(content), chunk_size):
366
- if operation.state == SearchState.CANCELLED:
367
- break
368
-
369
- chunk = content[i:i + chunk_size]
370
- chunk_matches = []
371
-
372
- for match in pattern.finditer(chunk):
373
- if len(matches) >= operation.max_matches:
374
- break
375
-
376
- highlight_match = HighlightMatch(
377
- start=i + match.start(),
378
- end=i + match.end(),
379
- text=match.group(),
380
- tag_name=operation.tag_name
381
- )
382
-
383
- matches.append(highlight_match)
384
- chunk_matches.append(highlight_match)
385
-
386
- # Apply highlights for this chunk
387
- if chunk_matches:
388
- self._apply_highlights_batch(operation, chunk_matches)
389
-
390
- # Update progress
391
- operation.progress.matches_found = len(matches)
392
- operation.progress.processed_chars = min(i + chunk_size, len(content))
393
- operation.progress.batches_completed += 1
394
-
395
- # Call progress callback
396
- if operation.progress_callback:
397
- operation.progress_callback(operation)
398
-
399
- # Small delay to prevent UI blocking
400
- time.sleep(0.001)
401
-
402
- operation.matches = matches
403
-
404
- def _find_matches_lazy(self, operation: SearchOperation, pattern: re.Pattern, content: str):
405
- """Find matches only in visible area (lazy loading)."""
406
- # Get visible area of text widget
407
- try:
408
- visible_start = operation.text_widget.index("@0,0")
409
- visible_end = operation.text_widget.index(f"@{operation.text_widget.winfo_width()},{operation.text_widget.winfo_height()}")
410
-
411
- start_idx = operation.text_widget.count("1.0", visible_start, "chars")[0]
412
- end_idx = operation.text_widget.count("1.0", visible_end, "chars")[0]
413
-
414
- visible_content = content[start_idx:end_idx]
415
-
416
- except (tk.TclError, TypeError):
417
- # Fallback to processing entire content
418
- visible_content = content
419
- start_idx = 0
420
-
421
- matches = []
422
-
423
- for match in pattern.finditer(visible_content):
424
- if len(matches) >= operation.max_matches:
425
- break
426
-
427
- highlight_match = HighlightMatch(
428
- start=start_idx + match.start(),
429
- end=start_idx + match.end(),
430
- text=match.group(),
431
- tag_name=operation.tag_name
432
- )
433
- matches.append(highlight_match)
434
-
435
- operation.matches = matches
436
- operation.progress.matches_found = len(matches)
437
- operation.progress.processed_chars = len(visible_content)
438
-
439
- # Apply highlights
440
- self._apply_highlights_batch(operation, matches)
441
-
442
- def _apply_highlights_immediate(self, operation: SearchOperation):
443
- """Apply all highlights immediately."""
444
- def apply():
445
- for match in operation.matches:
446
- try:
447
- start_pos = f"1.0 + {match.start}c"
448
- end_pos = f"1.0 + {match.end}c"
449
- operation.text_widget.tag_add(match.tag_name, start_pos, end_pos)
450
- except tk.TclError:
451
- continue
452
-
453
- # Schedule on main thread
454
- operation.text_widget.after_idle(apply)
455
-
456
- def _apply_highlights_batch(self, operation: SearchOperation, matches: List[HighlightMatch]):
457
- """Apply highlights in a batch."""
458
- def apply():
459
- for match in matches:
460
- try:
461
- start_pos = f"1.0 + {match.start}c"
462
- end_pos = f"1.0 + {match.end}c"
463
- operation.text_widget.tag_add(match.tag_name, start_pos, end_pos)
464
- except tk.TclError:
465
- continue
466
-
467
- # Schedule on main thread
468
- operation.text_widget.after_idle(apply)
469
-
470
- def cancel_operation(self, operation_id: str) -> bool:
471
- """Cancel a running search operation."""
472
- with self.operation_lock:
473
- if operation_id in self.active_operations:
474
- operation = self.active_operations[operation_id]
475
- operation.state = SearchState.CANCELLED
476
- self.performance_stats['cancelled_operations'] += 1
477
- return True
478
- return False
479
-
480
- def cancel_all_operations(self):
481
- """Cancel all running search operations."""
482
- with self.operation_lock:
483
- for operation in self.active_operations.values():
484
- operation.state = SearchState.CANCELLED
485
- self.performance_stats['cancelled_operations'] += len(self.active_operations)
486
- self.active_operations.clear()
487
-
488
- def clear_highlights(self, text_widget: tk.Text, tag_name: str):
489
- """Clear all highlights for a specific tag."""
490
- def clear():
491
- try:
492
- text_widget.tag_remove(tag_name, "1.0", tk.END)
493
- except tk.TclError:
494
- pass
495
-
496
- text_widget.after_idle(clear)
497
-
498
- def clear_all_highlights(self, text_widget: tk.Text):
499
- """Clear all highlights in the text widget."""
500
- def clear():
501
- try:
502
- for tag_name in self.tag_configs.keys():
503
- text_widget.tag_remove(tag_name, "1.0", tk.END)
504
- except tk.TclError:
505
- pass
506
-
507
- text_widget.after_idle(clear)
508
-
509
- def get_operation_status(self, operation_id: str) -> Optional[SearchOperation]:
510
- """Get the status of a search operation."""
511
- with self.operation_lock:
512
- return self.active_operations.get(operation_id)
513
-
514
- def get_active_operations(self) -> List[str]:
515
- """Get list of active operation IDs."""
516
- with self.operation_lock:
517
- return list(self.active_operations.keys())
518
-
519
- def get_performance_stats(self) -> Dict[str, Any]:
520
- """Get performance statistics."""
521
- with self.operation_lock:
522
- return self.performance_stats.copy()
523
-
524
- def configure_tag(self, tag_name: str, **config):
525
- """Configure a highlight tag."""
526
- self.tag_configs[tag_name] = config
527
-
528
- def shutdown(self):
529
- """Shutdown the highlighter and cleanup resources."""
530
- self.cancel_all_operations()
531
- self.shutdown_event.set()
532
-
533
- if self.worker_thread and self.worker_thread.is_alive():
534
- # Signal shutdown
535
- self.operation_queue.put(None)
536
- self.worker_thread.join(timeout=2.0)
537
-
538
- # Global instance
539
- _global_search_highlighter = None
540
-
541
- def get_search_highlighter() -> OptimizedSearchHighlighter:
542
- """Get the global search highlighter instance."""
543
- global _global_search_highlighter
544
- if _global_search_highlighter is None:
545
- _global_search_highlighter = OptimizedSearchHighlighter()
546
- return _global_search_highlighter
547
-
548
- def shutdown_search_highlighter():
549
- """Shutdown the global search highlighter."""
550
- global _global_search_highlighter
551
- if _global_search_highlighter is not None:
552
- _global_search_highlighter.shutdown()
1
+ #!/usr/bin/env python3
2
+ """
3
+ Optimized search and highlighting system for text widgets.
4
+ Implements progressive highlighting, batching, and non-blocking operations.
5
+ """
6
+
7
+ import re
8
+ import time
9
+ import threading
10
+ import tkinter as tk
11
+ from typing import Dict, List, Optional, Any, Tuple, Generator, Callable
12
+ from dataclasses import dataclass, field
13
+ from collections import deque
14
+ from enum import Enum
15
+ import queue
16
+
17
+ class HighlightMode(Enum):
18
+ """Different highlighting modes for optimization."""
19
+ IMMEDIATE = "immediate" # Highlight all matches immediately
20
+ PROGRESSIVE = "progressive" # Highlight matches progressively
21
+ BATCH = "batch" # Highlight in batches
22
+ LAZY = "lazy" # Highlight only visible area
23
+
24
+ class SearchState(Enum):
25
+ """Search operation states."""
26
+ IDLE = "idle"
27
+ SEARCHING = "searching"
28
+ HIGHLIGHTING = "highlighting"
29
+ COMPLETED = "completed"
30
+ CANCELLED = "cancelled"
31
+ ERROR = "error"
32
+
33
+ @dataclass
34
+ class HighlightMatch:
35
+ """Represents a single highlight match."""
36
+ start: int
37
+ end: int
38
+ text: str
39
+ tag_name: str
40
+ priority: int = 0
41
+
42
+ @property
43
+ def length(self) -> int:
44
+ return self.end - self.start
45
+
46
+ @dataclass
47
+ class SearchProgress:
48
+ """Progress information for search operations."""
49
+ total_chars: int = 0
50
+ processed_chars: int = 0
51
+ matches_found: int = 0
52
+ batches_completed: int = 0
53
+ time_elapsed: float = 0.0
54
+ estimated_remaining: float = 0.0
55
+
56
+ @property
57
+ def progress_percent(self) -> float:
58
+ if self.total_chars == 0:
59
+ return 0.0
60
+ return (self.processed_chars / self.total_chars) * 100
61
+
62
+ @dataclass
63
+ class SearchOperation:
64
+ """Represents a search operation with its parameters."""
65
+ operation_id: str
66
+ pattern: str
67
+ text_widget: tk.Text
68
+ tag_name: str
69
+ flags: int = 0
70
+ mode: HighlightMode = HighlightMode.PROGRESSIVE
71
+ batch_size: int = 100
72
+ max_matches: int = 10000
73
+ timeout_ms: int = 5000
74
+
75
+ # State
76
+ state: SearchState = SearchState.IDLE
77
+ matches: List[HighlightMatch] = field(default_factory=list)
78
+ progress: SearchProgress = field(default_factory=SearchProgress)
79
+ start_time: float = field(default_factory=time.time)
80
+
81
+ # Callbacks
82
+ progress_callback: Optional[Callable] = None
83
+ completion_callback: Optional[Callable] = None
84
+ error_callback: Optional[Callable] = None
85
+
86
+ class OptimizedSearchHighlighter:
87
+ """
88
+ High-performance search and highlighting system with progressive updates,
89
+ batching, and non-blocking operations for large text documents.
90
+ """
91
+
92
+ def __init__(self,
93
+ default_batch_size: int = 100,
94
+ max_concurrent_operations: int = 3,
95
+ highlight_timeout_ms: int = 5000):
96
+
97
+ self.default_batch_size = default_batch_size
98
+ self.max_concurrent_operations = max_concurrent_operations
99
+ self.highlight_timeout_ms = highlight_timeout_ms
100
+
101
+ # Operation management
102
+ self.active_operations: Dict[str, SearchOperation] = {}
103
+ self.operation_queue = queue.Queue()
104
+ self.operation_lock = threading.RLock()
105
+
106
+ # Performance tracking
107
+ self.performance_stats = {
108
+ 'total_operations': 0,
109
+ 'completed_operations': 0,
110
+ 'cancelled_operations': 0,
111
+ 'error_operations': 0,
112
+ 'total_matches_found': 0,
113
+ 'total_processing_time': 0.0,
114
+ 'average_processing_time': 0.0
115
+ }
116
+
117
+ # Tag configuration
118
+ self.tag_configs = {
119
+ 'search_highlight': {'background': 'yellow', 'foreground': 'black'},
120
+ 'replace_highlight': {'background': 'pink', 'foreground': 'black'},
121
+ 'current_match': {'background': 'orange', 'foreground': 'black'},
122
+ 'error_highlight': {'background': 'red', 'foreground': 'white'},
123
+ 'yellow_highlight': {'background': 'yellow', 'foreground': 'black'},
124
+ 'pink_highlight': {'background': 'pink', 'foreground': 'black'}
125
+ }
126
+
127
+ # Worker thread for background processing
128
+ self.worker_thread = None
129
+ self.shutdown_event = threading.Event()
130
+ self._start_worker_thread()
131
+
132
+ def _start_worker_thread(self):
133
+ """Start the background worker thread for processing operations."""
134
+ if self.worker_thread is None or not self.worker_thread.is_alive():
135
+ self.worker_thread = threading.Thread(
136
+ target=self._worker_loop,
137
+ daemon=True,
138
+ name="SearchHighlighter-Worker"
139
+ )
140
+ self.worker_thread.start()
141
+
142
+ def _worker_loop(self):
143
+ """Main worker loop for processing search operations."""
144
+ while not self.shutdown_event.is_set():
145
+ try:
146
+ # Get next operation from queue (with timeout)
147
+ operation = self.operation_queue.get(timeout=1.0)
148
+ if operation is None: # Shutdown signal
149
+ break
150
+
151
+ self._process_operation(operation)
152
+
153
+ except queue.Empty:
154
+ continue
155
+ except Exception as e:
156
+ print(f"Error in search worker thread: {e}")
157
+
158
+ def search_and_highlight(self,
159
+ text_widget: tk.Text,
160
+ pattern: str,
161
+ tag_name: str = 'search_highlight',
162
+ mode: HighlightMode = HighlightMode.PROGRESSIVE,
163
+ flags: int = 0,
164
+ batch_size: Optional[int] = None,
165
+ max_matches: int = 10000,
166
+ progress_callback: Optional[Callable] = None,
167
+ completion_callback: Optional[Callable] = None) -> str:
168
+ """
169
+ Start a search and highlight operation.
170
+
171
+ Args:
172
+ text_widget: The tkinter Text widget to search in
173
+ pattern: Regular expression pattern to search for
174
+ tag_name: Tag name for highlighting matches
175
+ mode: Highlighting mode (immediate, progressive, batch, lazy)
176
+ flags: Regular expression flags
177
+ batch_size: Number of matches to process per batch
178
+ max_matches: Maximum number of matches to find
179
+ progress_callback: Callback for progress updates
180
+ completion_callback: Callback when operation completes
181
+
182
+ Returns:
183
+ Operation ID for tracking the operation
184
+ """
185
+ # Generate unique operation ID
186
+ operation_id = f"search_{int(time.time() * 1000000)}"
187
+
188
+ # Create search operation
189
+ operation = SearchOperation(
190
+ operation_id=operation_id,
191
+ pattern=pattern,
192
+ text_widget=text_widget,
193
+ tag_name=tag_name,
194
+ flags=flags,
195
+ mode=mode,
196
+ batch_size=batch_size or self.default_batch_size,
197
+ max_matches=max_matches,
198
+ timeout_ms=self.highlight_timeout_ms,
199
+ progress_callback=progress_callback,
200
+ completion_callback=completion_callback
201
+ )
202
+
203
+ # Configure tag if not already configured
204
+ self._configure_tag(text_widget, tag_name)
205
+
206
+ # Clear existing highlights for this tag
207
+ self.clear_highlights(text_widget, tag_name)
208
+
209
+ # Add to active operations
210
+ with self.operation_lock:
211
+ self.active_operations[operation_id] = operation
212
+ self.performance_stats['total_operations'] += 1
213
+
214
+ # Queue for processing
215
+ self.operation_queue.put(operation)
216
+
217
+ return operation_id
218
+
219
+ def _configure_tag(self, text_widget: tk.Text, tag_name: str):
220
+ """Configure highlighting tag in the text widget."""
221
+ if tag_name in self.tag_configs:
222
+ config = self.tag_configs[tag_name]
223
+ text_widget.tag_configure(tag_name, **config)
224
+ else:
225
+ # Default configuration
226
+ text_widget.tag_configure(tag_name, background='yellow', foreground='black')
227
+
228
+ def _process_operation(self, operation: SearchOperation):
229
+ """Process a search operation in the background."""
230
+ try:
231
+ operation.state = SearchState.SEARCHING
232
+ operation.start_time = time.time()
233
+
234
+ # Get text content
235
+ content = operation.text_widget.get("1.0", tk.END)
236
+ operation.progress.total_chars = len(content)
237
+
238
+ # Compile regex pattern
239
+ try:
240
+ compiled_pattern = re.compile(operation.pattern, operation.flags)
241
+ except re.error as e:
242
+ operation.state = SearchState.ERROR
243
+ if operation.error_callback:
244
+ operation.error_callback(operation, str(e))
245
+ return
246
+
247
+ # Find matches based on mode
248
+ if operation.mode == HighlightMode.IMMEDIATE:
249
+ self._find_all_matches_immediate(operation, compiled_pattern, content)
250
+ elif operation.mode == HighlightMode.PROGRESSIVE:
251
+ self._find_matches_progressive(operation, compiled_pattern, content)
252
+ elif operation.mode == HighlightMode.BATCH:
253
+ self._find_matches_batch(operation, compiled_pattern, content)
254
+ elif operation.mode == HighlightMode.LAZY:
255
+ self._find_matches_lazy(operation, compiled_pattern, content)
256
+
257
+ # Update performance stats
258
+ operation.progress.time_elapsed = time.time() - operation.start_time
259
+
260
+ with self.operation_lock:
261
+ if operation.state != SearchState.CANCELLED:
262
+ operation.state = SearchState.COMPLETED
263
+ self.performance_stats['completed_operations'] += 1
264
+ self.performance_stats['total_matches_found'] += len(operation.matches)
265
+ self.performance_stats['total_processing_time'] += operation.progress.time_elapsed
266
+
267
+ # Update average processing time
268
+ if self.performance_stats['completed_operations'] > 0:
269
+ self.performance_stats['average_processing_time'] = (
270
+ self.performance_stats['total_processing_time'] /
271
+ self.performance_stats['completed_operations']
272
+ )
273
+
274
+ # Remove from active operations
275
+ self.active_operations.pop(operation.operation_id, None)
276
+
277
+ # Call completion callback
278
+ if operation.completion_callback and operation.state == SearchState.COMPLETED:
279
+ operation.completion_callback(operation)
280
+
281
+ except Exception as e:
282
+ operation.state = SearchState.ERROR
283
+ with self.operation_lock:
284
+ self.performance_stats['error_operations'] += 1
285
+ self.active_operations.pop(operation.operation_id, None)
286
+
287
+ if operation.error_callback:
288
+ operation.error_callback(operation, str(e))
289
+
290
+ def _find_all_matches_immediate(self, operation: SearchOperation, pattern: re.Pattern, content: str):
291
+ """Find all matches immediately and highlight them."""
292
+ matches = []
293
+
294
+ for match in pattern.finditer(content):
295
+ if len(matches) >= operation.max_matches:
296
+ break
297
+
298
+ highlight_match = HighlightMatch(
299
+ start=match.start(),
300
+ end=match.end(),
301
+ text=match.group(),
302
+ tag_name=operation.tag_name
303
+ )
304
+ matches.append(highlight_match)
305
+
306
+ operation.matches = matches
307
+ operation.progress.matches_found = len(matches)
308
+ operation.progress.processed_chars = len(content)
309
+
310
+ # Apply highlights immediately
311
+ self._apply_highlights_immediate(operation)
312
+
313
+ def _find_matches_progressive(self, operation: SearchOperation, pattern: re.Pattern, content: str):
314
+ """Find matches progressively with periodic UI updates."""
315
+ matches = []
316
+ batch_matches = []
317
+ last_update_time = time.time()
318
+ update_interval = 0.1 # Update UI every 100ms
319
+
320
+ for match in pattern.finditer(content):
321
+ if operation.state == SearchState.CANCELLED:
322
+ break
323
+
324
+ if len(matches) >= operation.max_matches:
325
+ break
326
+
327
+ highlight_match = HighlightMatch(
328
+ start=match.start(),
329
+ end=match.end(),
330
+ text=match.group(),
331
+ tag_name=operation.tag_name
332
+ )
333
+
334
+ matches.append(highlight_match)
335
+ batch_matches.append(highlight_match)
336
+
337
+ # Update progress
338
+ operation.progress.matches_found = len(matches)
339
+ operation.progress.processed_chars = match.end()
340
+
341
+ # Apply highlights in batches
342
+ if (len(batch_matches) >= operation.batch_size or
343
+ time.time() - last_update_time > update_interval):
344
+
345
+ self._apply_highlights_batch(operation, batch_matches)
346
+ batch_matches = []
347
+ last_update_time = time.time()
348
+
349
+ # Call progress callback
350
+ if operation.progress_callback:
351
+ operation.progress_callback(operation)
352
+
353
+ # Apply remaining highlights
354
+ if batch_matches:
355
+ self._apply_highlights_batch(operation, batch_matches)
356
+
357
+ operation.matches = matches
358
+ operation.progress.processed_chars = len(content)
359
+
360
+ def _find_matches_batch(self, operation: SearchOperation, pattern: re.Pattern, content: str):
361
+ """Find matches in batches with controlled processing."""
362
+ matches = []
363
+ chunk_size = 10000 # Process 10KB chunks
364
+
365
+ for i in range(0, len(content), chunk_size):
366
+ if operation.state == SearchState.CANCELLED:
367
+ break
368
+
369
+ chunk = content[i:i + chunk_size]
370
+ chunk_matches = []
371
+
372
+ for match in pattern.finditer(chunk):
373
+ if len(matches) >= operation.max_matches:
374
+ break
375
+
376
+ highlight_match = HighlightMatch(
377
+ start=i + match.start(),
378
+ end=i + match.end(),
379
+ text=match.group(),
380
+ tag_name=operation.tag_name
381
+ )
382
+
383
+ matches.append(highlight_match)
384
+ chunk_matches.append(highlight_match)
385
+
386
+ # Apply highlights for this chunk
387
+ if chunk_matches:
388
+ self._apply_highlights_batch(operation, chunk_matches)
389
+
390
+ # Update progress
391
+ operation.progress.matches_found = len(matches)
392
+ operation.progress.processed_chars = min(i + chunk_size, len(content))
393
+ operation.progress.batches_completed += 1
394
+
395
+ # Call progress callback
396
+ if operation.progress_callback:
397
+ operation.progress_callback(operation)
398
+
399
+ # Small delay to prevent UI blocking
400
+ time.sleep(0.001)
401
+
402
+ operation.matches = matches
403
+
404
+ def _find_matches_lazy(self, operation: SearchOperation, pattern: re.Pattern, content: str):
405
+ """Find matches only in visible area (lazy loading)."""
406
+ # Get visible area of text widget
407
+ try:
408
+ visible_start = operation.text_widget.index("@0,0")
409
+ visible_end = operation.text_widget.index(f"@{operation.text_widget.winfo_width()},{operation.text_widget.winfo_height()}")
410
+
411
+ start_idx = operation.text_widget.count("1.0", visible_start, "chars")[0]
412
+ end_idx = operation.text_widget.count("1.0", visible_end, "chars")[0]
413
+
414
+ visible_content = content[start_idx:end_idx]
415
+
416
+ except (tk.TclError, TypeError):
417
+ # Fallback to processing entire content
418
+ visible_content = content
419
+ start_idx = 0
420
+
421
+ matches = []
422
+
423
+ for match in pattern.finditer(visible_content):
424
+ if len(matches) >= operation.max_matches:
425
+ break
426
+
427
+ highlight_match = HighlightMatch(
428
+ start=start_idx + match.start(),
429
+ end=start_idx + match.end(),
430
+ text=match.group(),
431
+ tag_name=operation.tag_name
432
+ )
433
+ matches.append(highlight_match)
434
+
435
+ operation.matches = matches
436
+ operation.progress.matches_found = len(matches)
437
+ operation.progress.processed_chars = len(visible_content)
438
+
439
+ # Apply highlights
440
+ self._apply_highlights_batch(operation, matches)
441
+
442
+ def _apply_highlights_immediate(self, operation: SearchOperation):
443
+ """Apply all highlights immediately."""
444
+ def apply():
445
+ for match in operation.matches:
446
+ try:
447
+ start_pos = f"1.0 + {match.start}c"
448
+ end_pos = f"1.0 + {match.end}c"
449
+ operation.text_widget.tag_add(match.tag_name, start_pos, end_pos)
450
+ except tk.TclError:
451
+ continue
452
+
453
+ # Schedule on main thread
454
+ operation.text_widget.after_idle(apply)
455
+
456
+ def _apply_highlights_batch(self, operation: SearchOperation, matches: List[HighlightMatch]):
457
+ """Apply highlights in a batch."""
458
+ def apply():
459
+ for match in matches:
460
+ try:
461
+ start_pos = f"1.0 + {match.start}c"
462
+ end_pos = f"1.0 + {match.end}c"
463
+ operation.text_widget.tag_add(match.tag_name, start_pos, end_pos)
464
+ except tk.TclError:
465
+ continue
466
+
467
+ # Schedule on main thread
468
+ operation.text_widget.after_idle(apply)
469
+
470
+ def cancel_operation(self, operation_id: str) -> bool:
471
+ """Cancel a running search operation."""
472
+ with self.operation_lock:
473
+ if operation_id in self.active_operations:
474
+ operation = self.active_operations[operation_id]
475
+ operation.state = SearchState.CANCELLED
476
+ self.performance_stats['cancelled_operations'] += 1
477
+ return True
478
+ return False
479
+
480
+ def cancel_all_operations(self):
481
+ """Cancel all running search operations."""
482
+ with self.operation_lock:
483
+ for operation in self.active_operations.values():
484
+ operation.state = SearchState.CANCELLED
485
+ self.performance_stats['cancelled_operations'] += len(self.active_operations)
486
+ self.active_operations.clear()
487
+
488
+ def clear_highlights(self, text_widget: tk.Text, tag_name: str):
489
+ """Clear all highlights for a specific tag."""
490
+ def clear():
491
+ try:
492
+ text_widget.tag_remove(tag_name, "1.0", tk.END)
493
+ except tk.TclError:
494
+ pass
495
+
496
+ text_widget.after_idle(clear)
497
+
498
+ def clear_all_highlights(self, text_widget: tk.Text):
499
+ """Clear all highlights in the text widget."""
500
+ def clear():
501
+ try:
502
+ for tag_name in self.tag_configs.keys():
503
+ text_widget.tag_remove(tag_name, "1.0", tk.END)
504
+ except tk.TclError:
505
+ pass
506
+
507
+ text_widget.after_idle(clear)
508
+
509
+ def get_operation_status(self, operation_id: str) -> Optional[SearchOperation]:
510
+ """Get the status of a search operation."""
511
+ with self.operation_lock:
512
+ return self.active_operations.get(operation_id)
513
+
514
+ def get_active_operations(self) -> List[str]:
515
+ """Get list of active operation IDs."""
516
+ with self.operation_lock:
517
+ return list(self.active_operations.keys())
518
+
519
+ def get_performance_stats(self) -> Dict[str, Any]:
520
+ """Get performance statistics."""
521
+ with self.operation_lock:
522
+ return self.performance_stats.copy()
523
+
524
+ def configure_tag(self, tag_name: str, **config):
525
+ """Configure a highlight tag."""
526
+ self.tag_configs[tag_name] = config
527
+
528
+ def shutdown(self):
529
+ """Shutdown the highlighter and cleanup resources."""
530
+ self.cancel_all_operations()
531
+ self.shutdown_event.set()
532
+
533
+ if self.worker_thread and self.worker_thread.is_alive():
534
+ # Signal shutdown
535
+ self.operation_queue.put(None)
536
+ self.worker_thread.join(timeout=2.0)
537
+
538
+ # Global instance
539
+ _global_search_highlighter = None
540
+
541
+ def get_search_highlighter() -> OptimizedSearchHighlighter:
542
+ """Get the global search highlighter instance."""
543
+ global _global_search_highlighter
544
+ if _global_search_highlighter is None:
545
+ _global_search_highlighter = OptimizedSearchHighlighter()
546
+ return _global_search_highlighter
547
+
548
+ def shutdown_search_highlighter():
549
+ """Shutdown the global search highlighter."""
550
+ global _global_search_highlighter
551
+ if _global_search_highlighter is not None:
552
+ _global_search_highlighter.shutdown()
553
553
  _global_search_highlighter = None