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.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1199 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_directory.py +549 -0
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +540 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2699 -2345
- package/core/memento.py +275 -0
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/migrate_data.py +127 -0
- package/package.json +64 -57
- package/pomera.py +7883 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +33 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1817 -1072
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +2289 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +978 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
|
@@ -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
|