pomera-ai-commander 0.1.0 → 1.2.1
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 +1033 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- 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/efficient_line_numbers.py +510 -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/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 +2372 -2345
- 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/package.json +61 -57
- package/pomera.py +7482 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +32 -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 +1071 -1071
- 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 +1750 -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 +973 -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,715 +1,715 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Optimized find and replace processor with chunked processing and progress feedback.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import tkinter as tk
|
|
7
|
-
import re
|
|
8
|
-
import time
|
|
9
|
-
import threading
|
|
10
|
-
import queue
|
|
11
|
-
from typing import Dict, List, Optional, Callable, Any
|
|
12
|
-
from dataclasses import dataclass, field
|
|
13
|
-
from enum import Enum
|
|
14
|
-
|
|
15
|
-
class ReplaceOperation(Enum):
|
|
16
|
-
"""Types of replace operations."""
|
|
17
|
-
SIMPLE = "simple"
|
|
18
|
-
REGEX = "regex"
|
|
19
|
-
|
|
20
|
-
class ProcessingMode(Enum):
|
|
21
|
-
"""Processing modes for find/replace operations."""
|
|
22
|
-
IMMEDIATE = "immediate"
|
|
23
|
-
CHUNKED = "chunked"
|
|
24
|
-
STREAMING = "streaming"
|
|
25
|
-
PREVIEW_ONLY = "preview_only"
|
|
26
|
-
|
|
27
|
-
@dataclass
|
|
28
|
-
class ReplaceMatch:
|
|
29
|
-
"""Represents a single find/replace match."""
|
|
30
|
-
start: int
|
|
31
|
-
end: int
|
|
32
|
-
original_text: str
|
|
33
|
-
replacement_text: str
|
|
34
|
-
match_number: int
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def length_change(self) -> int:
|
|
38
|
-
"""Calculate the change in text length after replacement."""
|
|
39
|
-
return len(self.replacement_text) - len(self.original_text)
|
|
40
|
-
|
|
41
|
-
@dataclass
|
|
42
|
-
class ProcessingProgress:
|
|
43
|
-
"""Progress information for find/replace operations."""
|
|
44
|
-
total_chars: int = 0
|
|
45
|
-
processed_chars: int = 0
|
|
46
|
-
matches_found: int = 0
|
|
47
|
-
matches_replaced: int = 0
|
|
48
|
-
chunks_completed: int = 0
|
|
49
|
-
time_elapsed: float = 0.0
|
|
50
|
-
estimated_remaining: float = 0.0
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def progress_percent(self) -> float:
|
|
54
|
-
if self.total_chars == 0:
|
|
55
|
-
return 0.0
|
|
56
|
-
return (self.processed_chars / self.total_chars) * 100
|
|
57
|
-
|
|
58
|
-
@dataclass
|
|
59
|
-
class FindReplaceOperation:
|
|
60
|
-
"""Represents a find/replace operation with its parameters."""
|
|
61
|
-
operation_id: str
|
|
62
|
-
find_pattern: str
|
|
63
|
-
replace_text: str
|
|
64
|
-
text_widget: tk.Text
|
|
65
|
-
operation_type: ReplaceOperation = ReplaceOperation.SIMPLE
|
|
66
|
-
processing_mode: ProcessingMode = ProcessingMode.CHUNKED
|
|
67
|
-
|
|
68
|
-
# Options
|
|
69
|
-
case_sensitive: bool = True
|
|
70
|
-
whole_words: bool = False
|
|
71
|
-
use_regex: bool = False
|
|
72
|
-
max_replacements: int = -1 # -1 for unlimited
|
|
73
|
-
chunk_size: int = 10000
|
|
74
|
-
|
|
75
|
-
# State
|
|
76
|
-
matches: List[ReplaceMatch] = field(default_factory=list)
|
|
77
|
-
progress: ProcessingProgress = field(default_factory=ProcessingProgress)
|
|
78
|
-
start_time: float = field(default_factory=time.time)
|
|
79
|
-
is_cancelled: bool = False
|
|
80
|
-
|
|
81
|
-
# Results
|
|
82
|
-
original_text: str = ""
|
|
83
|
-
processed_text: str = ""
|
|
84
|
-
|
|
85
|
-
# Callbacks
|
|
86
|
-
progress_callback: Optional[Callable] = None
|
|
87
|
-
completion_callback: Optional[Callable] = None
|
|
88
|
-
error_callback: Optional[Callable] = None
|
|
89
|
-
|
|
90
|
-
class OptimizedFindReplace:
|
|
91
|
-
"""
|
|
92
|
-
High-performance find and replace processor with chunked processing,
|
|
93
|
-
progress feedback, and efficient preview generation.
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
def __init__(self,
|
|
97
|
-
default_chunk_size: int = 10000,
|
|
98
|
-
max_concurrent_operations: int = 2,
|
|
99
|
-
progress_update_interval: float = 0.1):
|
|
100
|
-
|
|
101
|
-
self.default_chunk_size = default_chunk_size
|
|
102
|
-
self.max_concurrent_operations = max_concurrent_operations
|
|
103
|
-
self.progress_update_interval = progress_update_interval
|
|
104
|
-
|
|
105
|
-
# Operation management
|
|
106
|
-
self.active_operations: Dict[str, FindReplaceOperation] = {}
|
|
107
|
-
self.operation_queue = queue.Queue()
|
|
108
|
-
self.operation_lock = threading.RLock()
|
|
109
|
-
|
|
110
|
-
# Performance tracking
|
|
111
|
-
self.performance_stats = {
|
|
112
|
-
'total_operations': 0,
|
|
113
|
-
'completed_operations': 0,
|
|
114
|
-
'cancelled_operations': 0,
|
|
115
|
-
'error_operations': 0,
|
|
116
|
-
'total_matches_processed': 0,
|
|
117
|
-
'total_processing_time': 0.0,
|
|
118
|
-
'average_processing_time': 0.0
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Worker thread for background processing
|
|
122
|
-
self.worker_thread = None
|
|
123
|
-
self.shutdown_event = threading.Event()
|
|
124
|
-
self._start_worker_thread()
|
|
125
|
-
|
|
126
|
-
def _start_worker_thread(self):
|
|
127
|
-
"""Start the background worker thread for processing operations."""
|
|
128
|
-
if self.worker_thread is None or not self.worker_thread.is_alive():
|
|
129
|
-
self.worker_thread = threading.Thread(
|
|
130
|
-
target=self._worker_loop,
|
|
131
|
-
daemon=True,
|
|
132
|
-
name="FindReplace-Worker"
|
|
133
|
-
)
|
|
134
|
-
self.worker_thread.start()
|
|
135
|
-
|
|
136
|
-
def _worker_loop(self):
|
|
137
|
-
"""Main worker loop for processing find/replace operations."""
|
|
138
|
-
while not self.shutdown_event.is_set():
|
|
139
|
-
try:
|
|
140
|
-
# Get next operation from queue (with timeout)
|
|
141
|
-
operation = self.operation_queue.get(timeout=1.0)
|
|
142
|
-
if operation is None: # Shutdown signal
|
|
143
|
-
break
|
|
144
|
-
|
|
145
|
-
self._process_operation(operation)
|
|
146
|
-
|
|
147
|
-
except queue.Empty:
|
|
148
|
-
continue
|
|
149
|
-
except Exception as e:
|
|
150
|
-
print(f"Error in find/replace worker thread: {e}")
|
|
151
|
-
|
|
152
|
-
def find_and_replace(self,
|
|
153
|
-
text_widget: tk.Text,
|
|
154
|
-
find_pattern: str,
|
|
155
|
-
replace_text: str,
|
|
156
|
-
case_sensitive: bool = True,
|
|
157
|
-
whole_words: bool = False,
|
|
158
|
-
use_regex: bool = False,
|
|
159
|
-
max_replacements: int = -1,
|
|
160
|
-
processing_mode: ProcessingMode = ProcessingMode.CHUNKED,
|
|
161
|
-
chunk_size: Optional[int] = None,
|
|
162
|
-
progress_callback: Optional[Callable] = None,
|
|
163
|
-
completion_callback: Optional[Callable] = None) -> str:
|
|
164
|
-
"""
|
|
165
|
-
Start a find and replace operation.
|
|
166
|
-
|
|
167
|
-
Args:
|
|
168
|
-
text_widget: The tkinter Text widget to process
|
|
169
|
-
find_pattern: Pattern to search for
|
|
170
|
-
replace_text: Text to replace matches with
|
|
171
|
-
case_sensitive: Whether search is case sensitive
|
|
172
|
-
whole_words: Whether to match whole words only
|
|
173
|
-
use_regex: Whether to use regular expressions
|
|
174
|
-
max_replacements: Maximum number of replacements (-1 for unlimited)
|
|
175
|
-
processing_mode: How to process the operation
|
|
176
|
-
chunk_size: Size of chunks for chunked processing
|
|
177
|
-
progress_callback: Callback for progress updates
|
|
178
|
-
completion_callback: Callback when operation completes
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
Operation ID for tracking the operation
|
|
182
|
-
"""
|
|
183
|
-
# Generate unique operation ID
|
|
184
|
-
operation_id = f"findreplace_{int(time.time() * 1000000)}"
|
|
185
|
-
|
|
186
|
-
# Determine operation type
|
|
187
|
-
if use_regex:
|
|
188
|
-
operation_type = ReplaceOperation.REGEX
|
|
189
|
-
else:
|
|
190
|
-
operation_type = ReplaceOperation.SIMPLE
|
|
191
|
-
|
|
192
|
-
# Create operation
|
|
193
|
-
operation = FindReplaceOperation(
|
|
194
|
-
operation_id=operation_id,
|
|
195
|
-
find_pattern=find_pattern,
|
|
196
|
-
replace_text=replace_text,
|
|
197
|
-
text_widget=text_widget,
|
|
198
|
-
operation_type=operation_type,
|
|
199
|
-
processing_mode=processing_mode,
|
|
200
|
-
case_sensitive=case_sensitive,
|
|
201
|
-
whole_words=whole_words,
|
|
202
|
-
use_regex=use_regex,
|
|
203
|
-
max_replacements=max_replacements,
|
|
204
|
-
chunk_size=chunk_size or self.default_chunk_size,
|
|
205
|
-
progress_callback=progress_callback,
|
|
206
|
-
completion_callback=completion_callback
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
# Get original text
|
|
210
|
-
operation.original_text = text_widget.get("1.0", tk.END)
|
|
211
|
-
operation.progress.total_chars = len(operation.original_text)
|
|
212
|
-
|
|
213
|
-
# Add to active operations
|
|
214
|
-
with self.operation_lock:
|
|
215
|
-
self.active_operations[operation_id] = operation
|
|
216
|
-
self.performance_stats['total_operations'] += 1
|
|
217
|
-
|
|
218
|
-
# Queue for processing
|
|
219
|
-
self.operation_queue.put(operation)
|
|
220
|
-
|
|
221
|
-
return operation_id
|
|
222
|
-
|
|
223
|
-
def generate_preview(self,
|
|
224
|
-
text_widget: tk.Text,
|
|
225
|
-
find_pattern: str,
|
|
226
|
-
replace_text: str,
|
|
227
|
-
case_sensitive: bool = True,
|
|
228
|
-
whole_words: bool = False,
|
|
229
|
-
use_regex: bool = False,
|
|
230
|
-
max_matches: int = 1000,
|
|
231
|
-
progress_callback: Optional[Callable] = None) -> str:
|
|
232
|
-
"""
|
|
233
|
-
Generate a preview of find/replace operation without modifying the text.
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
Operation ID for tracking the preview generation
|
|
237
|
-
"""
|
|
238
|
-
return self.find_and_replace(
|
|
239
|
-
text_widget=text_widget,
|
|
240
|
-
find_pattern=find_pattern,
|
|
241
|
-
replace_text=replace_text,
|
|
242
|
-
case_sensitive=case_sensitive,
|
|
243
|
-
whole_words=whole_words,
|
|
244
|
-
use_regex=use_regex,
|
|
245
|
-
max_replacements=max_matches,
|
|
246
|
-
processing_mode=ProcessingMode.PREVIEW_ONLY,
|
|
247
|
-
progress_callback=progress_callback
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
def _process_operation(self, operation: FindReplaceOperation):
|
|
251
|
-
"""Process a find/replace operation in the background."""
|
|
252
|
-
try:
|
|
253
|
-
operation.start_time = time.time()
|
|
254
|
-
|
|
255
|
-
# Build search pattern
|
|
256
|
-
pattern = self._build_search_pattern(operation)
|
|
257
|
-
if pattern is None:
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
# Process based on mode
|
|
261
|
-
if operation.processing_mode == ProcessingMode.IMMEDIATE:
|
|
262
|
-
self._process_immediate(operation, pattern)
|
|
263
|
-
elif operation.processing_mode == ProcessingMode.CHUNKED:
|
|
264
|
-
self._process_chunked(operation, pattern)
|
|
265
|
-
elif operation.processing_mode == ProcessingMode.STREAMING:
|
|
266
|
-
self._process_streaming(operation, pattern)
|
|
267
|
-
elif operation.processing_mode == ProcessingMode.PREVIEW_ONLY:
|
|
268
|
-
self._process_preview_only(operation, pattern)
|
|
269
|
-
|
|
270
|
-
# Update performance stats
|
|
271
|
-
operation.progress.time_elapsed = time.time() - operation.start_time
|
|
272
|
-
|
|
273
|
-
with self.operation_lock:
|
|
274
|
-
if not operation.is_cancelled:
|
|
275
|
-
self.performance_stats['completed_operations'] += 1
|
|
276
|
-
self.performance_stats['total_matches_processed'] += len(operation.matches)
|
|
277
|
-
self.performance_stats['total_processing_time'] += operation.progress.time_elapsed
|
|
278
|
-
|
|
279
|
-
# Update average processing time
|
|
280
|
-
if self.performance_stats['completed_operations'] > 0:
|
|
281
|
-
self.performance_stats['average_processing_time'] = (
|
|
282
|
-
self.performance_stats['total_processing_time'] /
|
|
283
|
-
self.performance_stats['completed_operations']
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
# Remove from active operations
|
|
287
|
-
self.active_operations.pop(operation.operation_id, None)
|
|
288
|
-
|
|
289
|
-
# Call completion callback
|
|
290
|
-
if operation.completion_callback and not operation.is_cancelled:
|
|
291
|
-
operation.completion_callback(operation)
|
|
292
|
-
|
|
293
|
-
except Exception as e:
|
|
294
|
-
with self.operation_lock:
|
|
295
|
-
self.performance_stats['error_operations'] += 1
|
|
296
|
-
self.active_operations.pop(operation.operation_id, None)
|
|
297
|
-
|
|
298
|
-
if operation.error_callback:
|
|
299
|
-
operation.error_callback(operation, str(e))
|
|
300
|
-
|
|
301
|
-
def _build_search_pattern(self, operation: FindReplaceOperation) -> Optional[re.Pattern]:
|
|
302
|
-
"""Build the search pattern based on operation parameters."""
|
|
303
|
-
try:
|
|
304
|
-
pattern_str = operation.find_pattern
|
|
305
|
-
|
|
306
|
-
# Handle whole words
|
|
307
|
-
if operation.whole_words and not operation.use_regex:
|
|
308
|
-
pattern_str = r'\b' + re.escape(pattern_str) + r'\b'
|
|
309
|
-
elif not operation.use_regex:
|
|
310
|
-
pattern_str = re.escape(pattern_str)
|
|
311
|
-
|
|
312
|
-
# Build flags
|
|
313
|
-
flags = 0
|
|
314
|
-
if not operation.case_sensitive:
|
|
315
|
-
flags |= re.IGNORECASE
|
|
316
|
-
|
|
317
|
-
return re.compile(pattern_str, flags)
|
|
318
|
-
|
|
319
|
-
except re.error as e:
|
|
320
|
-
if operation.error_callback:
|
|
321
|
-
operation.error_callback(operation, f"Invalid regex pattern: {e}")
|
|
322
|
-
return None
|
|
323
|
-
|
|
324
|
-
def _process_immediate(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
325
|
-
"""Process the entire text immediately."""
|
|
326
|
-
content = operation.original_text
|
|
327
|
-
matches = []
|
|
328
|
-
|
|
329
|
-
# Find all matches
|
|
330
|
-
for match_num, match in enumerate(pattern.finditer(content)):
|
|
331
|
-
if operation.is_cancelled:
|
|
332
|
-
break
|
|
333
|
-
|
|
334
|
-
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
335
|
-
break
|
|
336
|
-
|
|
337
|
-
replacement = self._get_replacement_text(operation, match)
|
|
338
|
-
|
|
339
|
-
replace_match = ReplaceMatch(
|
|
340
|
-
start=match.start(),
|
|
341
|
-
end=match.end(),
|
|
342
|
-
original_text=match.group(),
|
|
343
|
-
replacement_text=replacement,
|
|
344
|
-
match_number=match_num
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
matches.append(replace_match)
|
|
348
|
-
|
|
349
|
-
operation.matches = matches
|
|
350
|
-
operation.progress.matches_found = len(matches)
|
|
351
|
-
operation.progress.processed_chars = len(content)
|
|
352
|
-
|
|
353
|
-
# Apply replacements if not preview mode
|
|
354
|
-
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
355
|
-
operation.processed_text = self._apply_replacements(content, matches)
|
|
356
|
-
self._update_text_widget(operation)
|
|
357
|
-
|
|
358
|
-
def _process_chunked(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
359
|
-
"""Process text in chunks with progress updates."""
|
|
360
|
-
content = operation.original_text
|
|
361
|
-
matches = []
|
|
362
|
-
chunk_size = operation.chunk_size
|
|
363
|
-
last_update_time = time.time()
|
|
364
|
-
|
|
365
|
-
# Process in overlapping chunks to handle matches that span chunk boundaries
|
|
366
|
-
overlap = min(1000, chunk_size // 10) # 10% overlap or 1KB max
|
|
367
|
-
|
|
368
|
-
for i in range(0, len(content), chunk_size - overlap):
|
|
369
|
-
if operation.is_cancelled:
|
|
370
|
-
break
|
|
371
|
-
|
|
372
|
-
# Get chunk with overlap
|
|
373
|
-
chunk_start = i
|
|
374
|
-
chunk_end = min(i + chunk_size, len(content))
|
|
375
|
-
chunk = content[chunk_start:chunk_end]
|
|
376
|
-
|
|
377
|
-
# Find matches in chunk
|
|
378
|
-
chunk_matches = []
|
|
379
|
-
for match_num, match in enumerate(pattern.finditer(chunk)):
|
|
380
|
-
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
381
|
-
break
|
|
382
|
-
|
|
383
|
-
# Adjust match positions to global coordinates
|
|
384
|
-
global_start = chunk_start + match.start()
|
|
385
|
-
global_end = chunk_start + match.end()
|
|
386
|
-
|
|
387
|
-
# Skip if this match overlaps with previous chunk (avoid duplicates)
|
|
388
|
-
if i > 0 and global_start < i:
|
|
389
|
-
continue
|
|
390
|
-
|
|
391
|
-
replacement = self._get_replacement_text(operation, match)
|
|
392
|
-
|
|
393
|
-
replace_match = ReplaceMatch(
|
|
394
|
-
start=global_start,
|
|
395
|
-
end=global_end,
|
|
396
|
-
original_text=match.group(),
|
|
397
|
-
replacement_text=replacement,
|
|
398
|
-
match_number=len(matches)
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
matches.append(replace_match)
|
|
402
|
-
chunk_matches.append(replace_match)
|
|
403
|
-
|
|
404
|
-
# Update progress
|
|
405
|
-
operation.progress.matches_found = len(matches)
|
|
406
|
-
operation.progress.processed_chars = min(chunk_end, len(content))
|
|
407
|
-
operation.progress.chunks_completed += 1
|
|
408
|
-
|
|
409
|
-
# Call progress callback periodically
|
|
410
|
-
if (time.time() - last_update_time > self.progress_update_interval and
|
|
411
|
-
operation.progress_callback):
|
|
412
|
-
operation.progress_callback(operation)
|
|
413
|
-
last_update_time = time.time()
|
|
414
|
-
|
|
415
|
-
# Small delay to prevent UI blocking
|
|
416
|
-
time.sleep(0.001)
|
|
417
|
-
|
|
418
|
-
operation.matches = matches
|
|
419
|
-
operation.progress.processed_chars = len(content)
|
|
420
|
-
|
|
421
|
-
# Apply replacements if not preview mode
|
|
422
|
-
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
423
|
-
operation.processed_text = self._apply_replacements(content, matches)
|
|
424
|
-
self._update_text_widget(operation)
|
|
425
|
-
|
|
426
|
-
def _process_streaming(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
427
|
-
"""Process text with streaming updates."""
|
|
428
|
-
content = operation.original_text
|
|
429
|
-
matches = []
|
|
430
|
-
processed_text = content
|
|
431
|
-
offset = 0 # Track offset due to replacements
|
|
432
|
-
|
|
433
|
-
for match_num, match in enumerate(pattern.finditer(content)):
|
|
434
|
-
if operation.is_cancelled:
|
|
435
|
-
break
|
|
436
|
-
|
|
437
|
-
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
438
|
-
break
|
|
439
|
-
|
|
440
|
-
replacement = self._get_replacement_text(operation, match)
|
|
441
|
-
|
|
442
|
-
replace_match = ReplaceMatch(
|
|
443
|
-
start=match.start(),
|
|
444
|
-
end=match.end(),
|
|
445
|
-
original_text=match.group(),
|
|
446
|
-
replacement_text=replacement,
|
|
447
|
-
match_number=match_num
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
matches.append(replace_match)
|
|
451
|
-
|
|
452
|
-
# Apply replacement immediately if not preview mode
|
|
453
|
-
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
454
|
-
# Adjust positions for previous replacements
|
|
455
|
-
adjusted_start = match.start() + offset
|
|
456
|
-
adjusted_end = match.end() + offset
|
|
457
|
-
|
|
458
|
-
# Replace in processed text
|
|
459
|
-
processed_text = (
|
|
460
|
-
processed_text[:adjusted_start] +
|
|
461
|
-
replacement +
|
|
462
|
-
processed_text[adjusted_end:]
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
# Update offset
|
|
466
|
-
offset += len(replacement) - len(match.group())
|
|
467
|
-
|
|
468
|
-
# Update text widget periodically
|
|
469
|
-
if len(matches) % 10 == 0: # Every 10 matches
|
|
470
|
-
self._update_text_widget_partial(operation, processed_text)
|
|
471
|
-
|
|
472
|
-
# Update progress
|
|
473
|
-
operation.progress.matches_found = len(matches)
|
|
474
|
-
operation.progress.processed_chars = match.end()
|
|
475
|
-
|
|
476
|
-
# Call progress callback
|
|
477
|
-
if operation.progress_callback:
|
|
478
|
-
operation.progress_callback(operation)
|
|
479
|
-
|
|
480
|
-
operation.matches = matches
|
|
481
|
-
operation.processed_text = processed_text
|
|
482
|
-
operation.progress.processed_chars = len(content)
|
|
483
|
-
|
|
484
|
-
# Final update if not preview mode
|
|
485
|
-
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
486
|
-
self._update_text_widget(operation)
|
|
487
|
-
|
|
488
|
-
def _process_preview_only(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
489
|
-
"""Process for preview only - find matches but don't replace."""
|
|
490
|
-
content = operation.original_text
|
|
491
|
-
matches = []
|
|
492
|
-
|
|
493
|
-
# Limit matches for preview to avoid performance issues
|
|
494
|
-
max_preview_matches = min(operation.max_replacements if operation.max_replacements > 0 else 1000, 1000)
|
|
495
|
-
|
|
496
|
-
for match_num, match in enumerate(pattern.finditer(content)):
|
|
497
|
-
if operation.is_cancelled:
|
|
498
|
-
break
|
|
499
|
-
|
|
500
|
-
if len(matches) >= max_preview_matches:
|
|
501
|
-
break
|
|
502
|
-
|
|
503
|
-
replacement = self._get_replacement_text(operation, match)
|
|
504
|
-
|
|
505
|
-
replace_match = ReplaceMatch(
|
|
506
|
-
start=match.start(),
|
|
507
|
-
end=match.end(),
|
|
508
|
-
original_text=match.group(),
|
|
509
|
-
replacement_text=replacement,
|
|
510
|
-
match_number=match_num
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
matches.append(replace_match)
|
|
514
|
-
|
|
515
|
-
# Update progress periodically
|
|
516
|
-
if match_num % 100 == 0 and operation.progress_callback:
|
|
517
|
-
operation.progress.matches_found = len(matches)
|
|
518
|
-
operation.progress.processed_chars = match.end()
|
|
519
|
-
operation.progress_callback(operation)
|
|
520
|
-
|
|
521
|
-
operation.matches = matches
|
|
522
|
-
operation.progress.matches_found = len(matches)
|
|
523
|
-
operation.progress.processed_chars = len(content)
|
|
524
|
-
|
|
525
|
-
# Generate preview text
|
|
526
|
-
operation.processed_text = self._apply_replacements(content, matches)
|
|
527
|
-
|
|
528
|
-
def _get_replacement_text(self, operation: FindReplaceOperation, match: re.Match) -> str:
|
|
529
|
-
"""Get the replacement text for a match."""
|
|
530
|
-
if operation.operation_type == ReplaceOperation.REGEX:
|
|
531
|
-
try:
|
|
532
|
-
return match.expand(operation.replace_text)
|
|
533
|
-
except re.error:
|
|
534
|
-
return operation.replace_text
|
|
535
|
-
else:
|
|
536
|
-
return operation.replace_text
|
|
537
|
-
|
|
538
|
-
def _apply_replacements(self, content: str, matches: List[ReplaceMatch]) -> str:
|
|
539
|
-
"""Apply all replacements to the content."""
|
|
540
|
-
if not matches:
|
|
541
|
-
return content
|
|
542
|
-
|
|
543
|
-
# Sort matches by position (reverse order to maintain positions)
|
|
544
|
-
sorted_matches = sorted(matches, key=lambda m: m.start, reverse=True)
|
|
545
|
-
|
|
546
|
-
result = content
|
|
547
|
-
for match in sorted_matches:
|
|
548
|
-
result = result[:match.start] + match.replacement_text + result[match.end:]
|
|
549
|
-
|
|
550
|
-
return result
|
|
551
|
-
|
|
552
|
-
def _update_text_widget(self, operation: FindReplaceOperation):
|
|
553
|
-
"""Update the text widget with processed text."""
|
|
554
|
-
def update():
|
|
555
|
-
try:
|
|
556
|
-
operation.text_widget.config(state="normal")
|
|
557
|
-
operation.text_widget.delete("1.0", tk.END)
|
|
558
|
-
operation.text_widget.insert("1.0", operation.processed_text)
|
|
559
|
-
operation.text_widget.config(state="disabled")
|
|
560
|
-
except tk.TclError:
|
|
561
|
-
pass
|
|
562
|
-
|
|
563
|
-
operation.text_widget.after_idle(update)
|
|
564
|
-
|
|
565
|
-
def _update_text_widget_partial(self, operation: FindReplaceOperation, text: str):
|
|
566
|
-
"""Update the text widget with partial processed text."""
|
|
567
|
-
def update():
|
|
568
|
-
try:
|
|
569
|
-
operation.text_widget.config(state="normal")
|
|
570
|
-
operation.text_widget.delete("1.0", tk.END)
|
|
571
|
-
operation.text_widget.insert("1.0", text)
|
|
572
|
-
operation.text_widget.config(state="disabled")
|
|
573
|
-
except tk.TclError:
|
|
574
|
-
pass
|
|
575
|
-
|
|
576
|
-
operation.text_widget.after_idle(update)
|
|
577
|
-
|
|
578
|
-
def cancel_operation(self, operation_id: str) -> bool:
|
|
579
|
-
"""Cancel a running find/replace operation."""
|
|
580
|
-
with self.operation_lock:
|
|
581
|
-
if operation_id in self.active_operations:
|
|
582
|
-
operation = self.active_operations[operation_id]
|
|
583
|
-
operation.is_cancelled = True
|
|
584
|
-
self.performance_stats['cancelled_operations'] += 1
|
|
585
|
-
return True
|
|
586
|
-
return False
|
|
587
|
-
|
|
588
|
-
def cancel_all_operations(self):
|
|
589
|
-
"""Cancel all running find/replace operations."""
|
|
590
|
-
with self.operation_lock:
|
|
591
|
-
for operation in self.active_operations.values():
|
|
592
|
-
operation.is_cancelled = True
|
|
593
|
-
self.performance_stats['cancelled_operations'] += len(self.active_operations)
|
|
594
|
-
self.active_operations.clear()
|
|
595
|
-
|
|
596
|
-
def get_operation_status(self, operation_id: str) -> Optional[FindReplaceOperation]:
|
|
597
|
-
"""Get the status of a find/replace operation."""
|
|
598
|
-
with self.operation_lock:
|
|
599
|
-
return self.active_operations.get(operation_id)
|
|
600
|
-
|
|
601
|
-
def get_active_operations(self) -> List[str]:
|
|
602
|
-
"""Get list of active operation IDs."""
|
|
603
|
-
with self.operation_lock:
|
|
604
|
-
return list(self.active_operations.keys())
|
|
605
|
-
|
|
606
|
-
def get_performance_stats(self) -> Dict[str, Any]:
|
|
607
|
-
"""Get performance statistics."""
|
|
608
|
-
with self.operation_lock:
|
|
609
|
-
return self.performance_stats.copy()
|
|
610
|
-
|
|
611
|
-
def process_find_replace(self, text, find_pattern, replace_text, mode="Text", options=None):
|
|
612
|
-
"""
|
|
613
|
-
Process find and replace operation with compatibility for the main application.
|
|
614
|
-
|
|
615
|
-
Args:
|
|
616
|
-
text: Input text to process
|
|
617
|
-
find_pattern: Pattern to search for
|
|
618
|
-
replace_text: Text to replace matches with
|
|
619
|
-
mode: "Text" or "Regex"
|
|
620
|
-
options: Dictionary with options like ignore_case, whole_words, etc.
|
|
621
|
-
|
|
622
|
-
Returns:
|
|
623
|
-
Result object with processed_text, success, error_message, processing_time_ms
|
|
624
|
-
"""
|
|
625
|
-
from dataclasses import dataclass
|
|
626
|
-
import time
|
|
627
|
-
import re
|
|
628
|
-
|
|
629
|
-
@dataclass
|
|
630
|
-
class ProcessResult:
|
|
631
|
-
processed_text: str = ""
|
|
632
|
-
success: bool = True
|
|
633
|
-
error_message: str = ""
|
|
634
|
-
processing_time_ms: float = 0.0
|
|
635
|
-
|
|
636
|
-
start_time = time.time()
|
|
637
|
-
result = ProcessResult()
|
|
638
|
-
|
|
639
|
-
try:
|
|
640
|
-
if not options:
|
|
641
|
-
options = {}
|
|
642
|
-
|
|
643
|
-
# Convert options to our format
|
|
644
|
-
case_sensitive = not options.get('ignore_case', False)
|
|
645
|
-
whole_words = options.get('whole_words', False)
|
|
646
|
-
use_regex = (mode == "Regex")
|
|
647
|
-
|
|
648
|
-
# Process directly without using the async worker
|
|
649
|
-
if use_regex:
|
|
650
|
-
# Handle regex mode
|
|
651
|
-
try:
|
|
652
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
653
|
-
result.processed_text = re.sub(find_pattern, replace_text, text, flags=flags)
|
|
654
|
-
result.success = True
|
|
655
|
-
except re.error as e:
|
|
656
|
-
result.success = False
|
|
657
|
-
result.error_message = f"Regex error: {e}"
|
|
658
|
-
result.processed_text = text
|
|
659
|
-
else:
|
|
660
|
-
# Handle text mode
|
|
661
|
-
if whole_words:
|
|
662
|
-
# Whole words matching
|
|
663
|
-
pattern = r'\b' + re.escape(find_pattern) + r'\b'
|
|
664
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
665
|
-
try:
|
|
666
|
-
result.processed_text = re.sub(pattern, replace_text, text, flags=flags)
|
|
667
|
-
result.success = True
|
|
668
|
-
except re.error as e:
|
|
669
|
-
result.success = False
|
|
670
|
-
result.error_message = f"Whole words error: {e}"
|
|
671
|
-
result.processed_text = text
|
|
672
|
-
else:
|
|
673
|
-
# Simple text replacement
|
|
674
|
-
if case_sensitive:
|
|
675
|
-
result.processed_text = text.replace(find_pattern, replace_text)
|
|
676
|
-
else:
|
|
677
|
-
# Case-insensitive replacement
|
|
678
|
-
pattern = re.escape(find_pattern)
|
|
679
|
-
result.processed_text = re.sub(pattern, replace_text, text, flags=re.IGNORECASE)
|
|
680
|
-
result.success = True
|
|
681
|
-
|
|
682
|
-
except Exception as e:
|
|
683
|
-
result.success = False
|
|
684
|
-
result.error_message = str(e)
|
|
685
|
-
result.processed_text = text
|
|
686
|
-
|
|
687
|
-
result.processing_time_ms = (time.time() - start_time) * 1000
|
|
688
|
-
return result
|
|
689
|
-
|
|
690
|
-
def shutdown(self):
|
|
691
|
-
"""Shutdown the processor and cleanup resources."""
|
|
692
|
-
self.cancel_all_operations()
|
|
693
|
-
self.shutdown_event.set()
|
|
694
|
-
|
|
695
|
-
if self.worker_thread and self.worker_thread.is_alive():
|
|
696
|
-
# Signal shutdown
|
|
697
|
-
self.operation_queue.put(None)
|
|
698
|
-
self.worker_thread.join(timeout=2.0)
|
|
699
|
-
|
|
700
|
-
# Global instance
|
|
701
|
-
_global_find_replace_processor = None
|
|
702
|
-
|
|
703
|
-
def get_find_replace_processor() -> OptimizedFindReplace:
|
|
704
|
-
"""Get the global find/replace processor instance."""
|
|
705
|
-
global _global_find_replace_processor
|
|
706
|
-
if _global_find_replace_processor is None:
|
|
707
|
-
_global_find_replace_processor = OptimizedFindReplace()
|
|
708
|
-
return _global_find_replace_processor
|
|
709
|
-
|
|
710
|
-
def shutdown_find_replace_processor():
|
|
711
|
-
"""Shutdown the global find/replace processor."""
|
|
712
|
-
global _global_find_replace_processor
|
|
713
|
-
if _global_find_replace_processor is not None:
|
|
714
|
-
_global_find_replace_processor.shutdown()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Optimized find and replace processor with chunked processing and progress feedback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
import queue
|
|
11
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
class ReplaceOperation(Enum):
|
|
16
|
+
"""Types of replace operations."""
|
|
17
|
+
SIMPLE = "simple"
|
|
18
|
+
REGEX = "regex"
|
|
19
|
+
|
|
20
|
+
class ProcessingMode(Enum):
|
|
21
|
+
"""Processing modes for find/replace operations."""
|
|
22
|
+
IMMEDIATE = "immediate"
|
|
23
|
+
CHUNKED = "chunked"
|
|
24
|
+
STREAMING = "streaming"
|
|
25
|
+
PREVIEW_ONLY = "preview_only"
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ReplaceMatch:
|
|
29
|
+
"""Represents a single find/replace match."""
|
|
30
|
+
start: int
|
|
31
|
+
end: int
|
|
32
|
+
original_text: str
|
|
33
|
+
replacement_text: str
|
|
34
|
+
match_number: int
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def length_change(self) -> int:
|
|
38
|
+
"""Calculate the change in text length after replacement."""
|
|
39
|
+
return len(self.replacement_text) - len(self.original_text)
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ProcessingProgress:
|
|
43
|
+
"""Progress information for find/replace operations."""
|
|
44
|
+
total_chars: int = 0
|
|
45
|
+
processed_chars: int = 0
|
|
46
|
+
matches_found: int = 0
|
|
47
|
+
matches_replaced: int = 0
|
|
48
|
+
chunks_completed: int = 0
|
|
49
|
+
time_elapsed: float = 0.0
|
|
50
|
+
estimated_remaining: float = 0.0
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def progress_percent(self) -> float:
|
|
54
|
+
if self.total_chars == 0:
|
|
55
|
+
return 0.0
|
|
56
|
+
return (self.processed_chars / self.total_chars) * 100
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class FindReplaceOperation:
|
|
60
|
+
"""Represents a find/replace operation with its parameters."""
|
|
61
|
+
operation_id: str
|
|
62
|
+
find_pattern: str
|
|
63
|
+
replace_text: str
|
|
64
|
+
text_widget: tk.Text
|
|
65
|
+
operation_type: ReplaceOperation = ReplaceOperation.SIMPLE
|
|
66
|
+
processing_mode: ProcessingMode = ProcessingMode.CHUNKED
|
|
67
|
+
|
|
68
|
+
# Options
|
|
69
|
+
case_sensitive: bool = True
|
|
70
|
+
whole_words: bool = False
|
|
71
|
+
use_regex: bool = False
|
|
72
|
+
max_replacements: int = -1 # -1 for unlimited
|
|
73
|
+
chunk_size: int = 10000
|
|
74
|
+
|
|
75
|
+
# State
|
|
76
|
+
matches: List[ReplaceMatch] = field(default_factory=list)
|
|
77
|
+
progress: ProcessingProgress = field(default_factory=ProcessingProgress)
|
|
78
|
+
start_time: float = field(default_factory=time.time)
|
|
79
|
+
is_cancelled: bool = False
|
|
80
|
+
|
|
81
|
+
# Results
|
|
82
|
+
original_text: str = ""
|
|
83
|
+
processed_text: str = ""
|
|
84
|
+
|
|
85
|
+
# Callbacks
|
|
86
|
+
progress_callback: Optional[Callable] = None
|
|
87
|
+
completion_callback: Optional[Callable] = None
|
|
88
|
+
error_callback: Optional[Callable] = None
|
|
89
|
+
|
|
90
|
+
class OptimizedFindReplace:
|
|
91
|
+
"""
|
|
92
|
+
High-performance find and replace processor with chunked processing,
|
|
93
|
+
progress feedback, and efficient preview generation.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self,
|
|
97
|
+
default_chunk_size: int = 10000,
|
|
98
|
+
max_concurrent_operations: int = 2,
|
|
99
|
+
progress_update_interval: float = 0.1):
|
|
100
|
+
|
|
101
|
+
self.default_chunk_size = default_chunk_size
|
|
102
|
+
self.max_concurrent_operations = max_concurrent_operations
|
|
103
|
+
self.progress_update_interval = progress_update_interval
|
|
104
|
+
|
|
105
|
+
# Operation management
|
|
106
|
+
self.active_operations: Dict[str, FindReplaceOperation] = {}
|
|
107
|
+
self.operation_queue = queue.Queue()
|
|
108
|
+
self.operation_lock = threading.RLock()
|
|
109
|
+
|
|
110
|
+
# Performance tracking
|
|
111
|
+
self.performance_stats = {
|
|
112
|
+
'total_operations': 0,
|
|
113
|
+
'completed_operations': 0,
|
|
114
|
+
'cancelled_operations': 0,
|
|
115
|
+
'error_operations': 0,
|
|
116
|
+
'total_matches_processed': 0,
|
|
117
|
+
'total_processing_time': 0.0,
|
|
118
|
+
'average_processing_time': 0.0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Worker thread for background processing
|
|
122
|
+
self.worker_thread = None
|
|
123
|
+
self.shutdown_event = threading.Event()
|
|
124
|
+
self._start_worker_thread()
|
|
125
|
+
|
|
126
|
+
def _start_worker_thread(self):
|
|
127
|
+
"""Start the background worker thread for processing operations."""
|
|
128
|
+
if self.worker_thread is None or not self.worker_thread.is_alive():
|
|
129
|
+
self.worker_thread = threading.Thread(
|
|
130
|
+
target=self._worker_loop,
|
|
131
|
+
daemon=True,
|
|
132
|
+
name="FindReplace-Worker"
|
|
133
|
+
)
|
|
134
|
+
self.worker_thread.start()
|
|
135
|
+
|
|
136
|
+
def _worker_loop(self):
|
|
137
|
+
"""Main worker loop for processing find/replace operations."""
|
|
138
|
+
while not self.shutdown_event.is_set():
|
|
139
|
+
try:
|
|
140
|
+
# Get next operation from queue (with timeout)
|
|
141
|
+
operation = self.operation_queue.get(timeout=1.0)
|
|
142
|
+
if operation is None: # Shutdown signal
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
self._process_operation(operation)
|
|
146
|
+
|
|
147
|
+
except queue.Empty:
|
|
148
|
+
continue
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"Error in find/replace worker thread: {e}")
|
|
151
|
+
|
|
152
|
+
def find_and_replace(self,
|
|
153
|
+
text_widget: tk.Text,
|
|
154
|
+
find_pattern: str,
|
|
155
|
+
replace_text: str,
|
|
156
|
+
case_sensitive: bool = True,
|
|
157
|
+
whole_words: bool = False,
|
|
158
|
+
use_regex: bool = False,
|
|
159
|
+
max_replacements: int = -1,
|
|
160
|
+
processing_mode: ProcessingMode = ProcessingMode.CHUNKED,
|
|
161
|
+
chunk_size: Optional[int] = None,
|
|
162
|
+
progress_callback: Optional[Callable] = None,
|
|
163
|
+
completion_callback: Optional[Callable] = None) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Start a find and replace operation.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
text_widget: The tkinter Text widget to process
|
|
169
|
+
find_pattern: Pattern to search for
|
|
170
|
+
replace_text: Text to replace matches with
|
|
171
|
+
case_sensitive: Whether search is case sensitive
|
|
172
|
+
whole_words: Whether to match whole words only
|
|
173
|
+
use_regex: Whether to use regular expressions
|
|
174
|
+
max_replacements: Maximum number of replacements (-1 for unlimited)
|
|
175
|
+
processing_mode: How to process the operation
|
|
176
|
+
chunk_size: Size of chunks for chunked processing
|
|
177
|
+
progress_callback: Callback for progress updates
|
|
178
|
+
completion_callback: Callback when operation completes
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Operation ID for tracking the operation
|
|
182
|
+
"""
|
|
183
|
+
# Generate unique operation ID
|
|
184
|
+
operation_id = f"findreplace_{int(time.time() * 1000000)}"
|
|
185
|
+
|
|
186
|
+
# Determine operation type
|
|
187
|
+
if use_regex:
|
|
188
|
+
operation_type = ReplaceOperation.REGEX
|
|
189
|
+
else:
|
|
190
|
+
operation_type = ReplaceOperation.SIMPLE
|
|
191
|
+
|
|
192
|
+
# Create operation
|
|
193
|
+
operation = FindReplaceOperation(
|
|
194
|
+
operation_id=operation_id,
|
|
195
|
+
find_pattern=find_pattern,
|
|
196
|
+
replace_text=replace_text,
|
|
197
|
+
text_widget=text_widget,
|
|
198
|
+
operation_type=operation_type,
|
|
199
|
+
processing_mode=processing_mode,
|
|
200
|
+
case_sensitive=case_sensitive,
|
|
201
|
+
whole_words=whole_words,
|
|
202
|
+
use_regex=use_regex,
|
|
203
|
+
max_replacements=max_replacements,
|
|
204
|
+
chunk_size=chunk_size or self.default_chunk_size,
|
|
205
|
+
progress_callback=progress_callback,
|
|
206
|
+
completion_callback=completion_callback
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Get original text
|
|
210
|
+
operation.original_text = text_widget.get("1.0", tk.END)
|
|
211
|
+
operation.progress.total_chars = len(operation.original_text)
|
|
212
|
+
|
|
213
|
+
# Add to active operations
|
|
214
|
+
with self.operation_lock:
|
|
215
|
+
self.active_operations[operation_id] = operation
|
|
216
|
+
self.performance_stats['total_operations'] += 1
|
|
217
|
+
|
|
218
|
+
# Queue for processing
|
|
219
|
+
self.operation_queue.put(operation)
|
|
220
|
+
|
|
221
|
+
return operation_id
|
|
222
|
+
|
|
223
|
+
def generate_preview(self,
|
|
224
|
+
text_widget: tk.Text,
|
|
225
|
+
find_pattern: str,
|
|
226
|
+
replace_text: str,
|
|
227
|
+
case_sensitive: bool = True,
|
|
228
|
+
whole_words: bool = False,
|
|
229
|
+
use_regex: bool = False,
|
|
230
|
+
max_matches: int = 1000,
|
|
231
|
+
progress_callback: Optional[Callable] = None) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Generate a preview of find/replace operation without modifying the text.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Operation ID for tracking the preview generation
|
|
237
|
+
"""
|
|
238
|
+
return self.find_and_replace(
|
|
239
|
+
text_widget=text_widget,
|
|
240
|
+
find_pattern=find_pattern,
|
|
241
|
+
replace_text=replace_text,
|
|
242
|
+
case_sensitive=case_sensitive,
|
|
243
|
+
whole_words=whole_words,
|
|
244
|
+
use_regex=use_regex,
|
|
245
|
+
max_replacements=max_matches,
|
|
246
|
+
processing_mode=ProcessingMode.PREVIEW_ONLY,
|
|
247
|
+
progress_callback=progress_callback
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _process_operation(self, operation: FindReplaceOperation):
|
|
251
|
+
"""Process a find/replace operation in the background."""
|
|
252
|
+
try:
|
|
253
|
+
operation.start_time = time.time()
|
|
254
|
+
|
|
255
|
+
# Build search pattern
|
|
256
|
+
pattern = self._build_search_pattern(operation)
|
|
257
|
+
if pattern is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Process based on mode
|
|
261
|
+
if operation.processing_mode == ProcessingMode.IMMEDIATE:
|
|
262
|
+
self._process_immediate(operation, pattern)
|
|
263
|
+
elif operation.processing_mode == ProcessingMode.CHUNKED:
|
|
264
|
+
self._process_chunked(operation, pattern)
|
|
265
|
+
elif operation.processing_mode == ProcessingMode.STREAMING:
|
|
266
|
+
self._process_streaming(operation, pattern)
|
|
267
|
+
elif operation.processing_mode == ProcessingMode.PREVIEW_ONLY:
|
|
268
|
+
self._process_preview_only(operation, pattern)
|
|
269
|
+
|
|
270
|
+
# Update performance stats
|
|
271
|
+
operation.progress.time_elapsed = time.time() - operation.start_time
|
|
272
|
+
|
|
273
|
+
with self.operation_lock:
|
|
274
|
+
if not operation.is_cancelled:
|
|
275
|
+
self.performance_stats['completed_operations'] += 1
|
|
276
|
+
self.performance_stats['total_matches_processed'] += len(operation.matches)
|
|
277
|
+
self.performance_stats['total_processing_time'] += operation.progress.time_elapsed
|
|
278
|
+
|
|
279
|
+
# Update average processing time
|
|
280
|
+
if self.performance_stats['completed_operations'] > 0:
|
|
281
|
+
self.performance_stats['average_processing_time'] = (
|
|
282
|
+
self.performance_stats['total_processing_time'] /
|
|
283
|
+
self.performance_stats['completed_operations']
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Remove from active operations
|
|
287
|
+
self.active_operations.pop(operation.operation_id, None)
|
|
288
|
+
|
|
289
|
+
# Call completion callback
|
|
290
|
+
if operation.completion_callback and not operation.is_cancelled:
|
|
291
|
+
operation.completion_callback(operation)
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
with self.operation_lock:
|
|
295
|
+
self.performance_stats['error_operations'] += 1
|
|
296
|
+
self.active_operations.pop(operation.operation_id, None)
|
|
297
|
+
|
|
298
|
+
if operation.error_callback:
|
|
299
|
+
operation.error_callback(operation, str(e))
|
|
300
|
+
|
|
301
|
+
def _build_search_pattern(self, operation: FindReplaceOperation) -> Optional[re.Pattern]:
|
|
302
|
+
"""Build the search pattern based on operation parameters."""
|
|
303
|
+
try:
|
|
304
|
+
pattern_str = operation.find_pattern
|
|
305
|
+
|
|
306
|
+
# Handle whole words
|
|
307
|
+
if operation.whole_words and not operation.use_regex:
|
|
308
|
+
pattern_str = r'\b' + re.escape(pattern_str) + r'\b'
|
|
309
|
+
elif not operation.use_regex:
|
|
310
|
+
pattern_str = re.escape(pattern_str)
|
|
311
|
+
|
|
312
|
+
# Build flags
|
|
313
|
+
flags = 0
|
|
314
|
+
if not operation.case_sensitive:
|
|
315
|
+
flags |= re.IGNORECASE
|
|
316
|
+
|
|
317
|
+
return re.compile(pattern_str, flags)
|
|
318
|
+
|
|
319
|
+
except re.error as e:
|
|
320
|
+
if operation.error_callback:
|
|
321
|
+
operation.error_callback(operation, f"Invalid regex pattern: {e}")
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def _process_immediate(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
325
|
+
"""Process the entire text immediately."""
|
|
326
|
+
content = operation.original_text
|
|
327
|
+
matches = []
|
|
328
|
+
|
|
329
|
+
# Find all matches
|
|
330
|
+
for match_num, match in enumerate(pattern.finditer(content)):
|
|
331
|
+
if operation.is_cancelled:
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
replacement = self._get_replacement_text(operation, match)
|
|
338
|
+
|
|
339
|
+
replace_match = ReplaceMatch(
|
|
340
|
+
start=match.start(),
|
|
341
|
+
end=match.end(),
|
|
342
|
+
original_text=match.group(),
|
|
343
|
+
replacement_text=replacement,
|
|
344
|
+
match_number=match_num
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
matches.append(replace_match)
|
|
348
|
+
|
|
349
|
+
operation.matches = matches
|
|
350
|
+
operation.progress.matches_found = len(matches)
|
|
351
|
+
operation.progress.processed_chars = len(content)
|
|
352
|
+
|
|
353
|
+
# Apply replacements if not preview mode
|
|
354
|
+
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
355
|
+
operation.processed_text = self._apply_replacements(content, matches)
|
|
356
|
+
self._update_text_widget(operation)
|
|
357
|
+
|
|
358
|
+
def _process_chunked(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
359
|
+
"""Process text in chunks with progress updates."""
|
|
360
|
+
content = operation.original_text
|
|
361
|
+
matches = []
|
|
362
|
+
chunk_size = operation.chunk_size
|
|
363
|
+
last_update_time = time.time()
|
|
364
|
+
|
|
365
|
+
# Process in overlapping chunks to handle matches that span chunk boundaries
|
|
366
|
+
overlap = min(1000, chunk_size // 10) # 10% overlap or 1KB max
|
|
367
|
+
|
|
368
|
+
for i in range(0, len(content), chunk_size - overlap):
|
|
369
|
+
if operation.is_cancelled:
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
# Get chunk with overlap
|
|
373
|
+
chunk_start = i
|
|
374
|
+
chunk_end = min(i + chunk_size, len(content))
|
|
375
|
+
chunk = content[chunk_start:chunk_end]
|
|
376
|
+
|
|
377
|
+
# Find matches in chunk
|
|
378
|
+
chunk_matches = []
|
|
379
|
+
for match_num, match in enumerate(pattern.finditer(chunk)):
|
|
380
|
+
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
# Adjust match positions to global coordinates
|
|
384
|
+
global_start = chunk_start + match.start()
|
|
385
|
+
global_end = chunk_start + match.end()
|
|
386
|
+
|
|
387
|
+
# Skip if this match overlaps with previous chunk (avoid duplicates)
|
|
388
|
+
if i > 0 and global_start < i:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
replacement = self._get_replacement_text(operation, match)
|
|
392
|
+
|
|
393
|
+
replace_match = ReplaceMatch(
|
|
394
|
+
start=global_start,
|
|
395
|
+
end=global_end,
|
|
396
|
+
original_text=match.group(),
|
|
397
|
+
replacement_text=replacement,
|
|
398
|
+
match_number=len(matches)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
matches.append(replace_match)
|
|
402
|
+
chunk_matches.append(replace_match)
|
|
403
|
+
|
|
404
|
+
# Update progress
|
|
405
|
+
operation.progress.matches_found = len(matches)
|
|
406
|
+
operation.progress.processed_chars = min(chunk_end, len(content))
|
|
407
|
+
operation.progress.chunks_completed += 1
|
|
408
|
+
|
|
409
|
+
# Call progress callback periodically
|
|
410
|
+
if (time.time() - last_update_time > self.progress_update_interval and
|
|
411
|
+
operation.progress_callback):
|
|
412
|
+
operation.progress_callback(operation)
|
|
413
|
+
last_update_time = time.time()
|
|
414
|
+
|
|
415
|
+
# Small delay to prevent UI blocking
|
|
416
|
+
time.sleep(0.001)
|
|
417
|
+
|
|
418
|
+
operation.matches = matches
|
|
419
|
+
operation.progress.processed_chars = len(content)
|
|
420
|
+
|
|
421
|
+
# Apply replacements if not preview mode
|
|
422
|
+
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
423
|
+
operation.processed_text = self._apply_replacements(content, matches)
|
|
424
|
+
self._update_text_widget(operation)
|
|
425
|
+
|
|
426
|
+
def _process_streaming(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
427
|
+
"""Process text with streaming updates."""
|
|
428
|
+
content = operation.original_text
|
|
429
|
+
matches = []
|
|
430
|
+
processed_text = content
|
|
431
|
+
offset = 0 # Track offset due to replacements
|
|
432
|
+
|
|
433
|
+
for match_num, match in enumerate(pattern.finditer(content)):
|
|
434
|
+
if operation.is_cancelled:
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
if operation.max_replacements > 0 and len(matches) >= operation.max_replacements:
|
|
438
|
+
break
|
|
439
|
+
|
|
440
|
+
replacement = self._get_replacement_text(operation, match)
|
|
441
|
+
|
|
442
|
+
replace_match = ReplaceMatch(
|
|
443
|
+
start=match.start(),
|
|
444
|
+
end=match.end(),
|
|
445
|
+
original_text=match.group(),
|
|
446
|
+
replacement_text=replacement,
|
|
447
|
+
match_number=match_num
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
matches.append(replace_match)
|
|
451
|
+
|
|
452
|
+
# Apply replacement immediately if not preview mode
|
|
453
|
+
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
454
|
+
# Adjust positions for previous replacements
|
|
455
|
+
adjusted_start = match.start() + offset
|
|
456
|
+
adjusted_end = match.end() + offset
|
|
457
|
+
|
|
458
|
+
# Replace in processed text
|
|
459
|
+
processed_text = (
|
|
460
|
+
processed_text[:adjusted_start] +
|
|
461
|
+
replacement +
|
|
462
|
+
processed_text[adjusted_end:]
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Update offset
|
|
466
|
+
offset += len(replacement) - len(match.group())
|
|
467
|
+
|
|
468
|
+
# Update text widget periodically
|
|
469
|
+
if len(matches) % 10 == 0: # Every 10 matches
|
|
470
|
+
self._update_text_widget_partial(operation, processed_text)
|
|
471
|
+
|
|
472
|
+
# Update progress
|
|
473
|
+
operation.progress.matches_found = len(matches)
|
|
474
|
+
operation.progress.processed_chars = match.end()
|
|
475
|
+
|
|
476
|
+
# Call progress callback
|
|
477
|
+
if operation.progress_callback:
|
|
478
|
+
operation.progress_callback(operation)
|
|
479
|
+
|
|
480
|
+
operation.matches = matches
|
|
481
|
+
operation.processed_text = processed_text
|
|
482
|
+
operation.progress.processed_chars = len(content)
|
|
483
|
+
|
|
484
|
+
# Final update if not preview mode
|
|
485
|
+
if operation.processing_mode != ProcessingMode.PREVIEW_ONLY:
|
|
486
|
+
self._update_text_widget(operation)
|
|
487
|
+
|
|
488
|
+
def _process_preview_only(self, operation: FindReplaceOperation, pattern: re.Pattern):
|
|
489
|
+
"""Process for preview only - find matches but don't replace."""
|
|
490
|
+
content = operation.original_text
|
|
491
|
+
matches = []
|
|
492
|
+
|
|
493
|
+
# Limit matches for preview to avoid performance issues
|
|
494
|
+
max_preview_matches = min(operation.max_replacements if operation.max_replacements > 0 else 1000, 1000)
|
|
495
|
+
|
|
496
|
+
for match_num, match in enumerate(pattern.finditer(content)):
|
|
497
|
+
if operation.is_cancelled:
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
if len(matches) >= max_preview_matches:
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
replacement = self._get_replacement_text(operation, match)
|
|
504
|
+
|
|
505
|
+
replace_match = ReplaceMatch(
|
|
506
|
+
start=match.start(),
|
|
507
|
+
end=match.end(),
|
|
508
|
+
original_text=match.group(),
|
|
509
|
+
replacement_text=replacement,
|
|
510
|
+
match_number=match_num
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
matches.append(replace_match)
|
|
514
|
+
|
|
515
|
+
# Update progress periodically
|
|
516
|
+
if match_num % 100 == 0 and operation.progress_callback:
|
|
517
|
+
operation.progress.matches_found = len(matches)
|
|
518
|
+
operation.progress.processed_chars = match.end()
|
|
519
|
+
operation.progress_callback(operation)
|
|
520
|
+
|
|
521
|
+
operation.matches = matches
|
|
522
|
+
operation.progress.matches_found = len(matches)
|
|
523
|
+
operation.progress.processed_chars = len(content)
|
|
524
|
+
|
|
525
|
+
# Generate preview text
|
|
526
|
+
operation.processed_text = self._apply_replacements(content, matches)
|
|
527
|
+
|
|
528
|
+
def _get_replacement_text(self, operation: FindReplaceOperation, match: re.Match) -> str:
|
|
529
|
+
"""Get the replacement text for a match."""
|
|
530
|
+
if operation.operation_type == ReplaceOperation.REGEX:
|
|
531
|
+
try:
|
|
532
|
+
return match.expand(operation.replace_text)
|
|
533
|
+
except re.error:
|
|
534
|
+
return operation.replace_text
|
|
535
|
+
else:
|
|
536
|
+
return operation.replace_text
|
|
537
|
+
|
|
538
|
+
def _apply_replacements(self, content: str, matches: List[ReplaceMatch]) -> str:
|
|
539
|
+
"""Apply all replacements to the content."""
|
|
540
|
+
if not matches:
|
|
541
|
+
return content
|
|
542
|
+
|
|
543
|
+
# Sort matches by position (reverse order to maintain positions)
|
|
544
|
+
sorted_matches = sorted(matches, key=lambda m: m.start, reverse=True)
|
|
545
|
+
|
|
546
|
+
result = content
|
|
547
|
+
for match in sorted_matches:
|
|
548
|
+
result = result[:match.start] + match.replacement_text + result[match.end:]
|
|
549
|
+
|
|
550
|
+
return result
|
|
551
|
+
|
|
552
|
+
def _update_text_widget(self, operation: FindReplaceOperation):
|
|
553
|
+
"""Update the text widget with processed text."""
|
|
554
|
+
def update():
|
|
555
|
+
try:
|
|
556
|
+
operation.text_widget.config(state="normal")
|
|
557
|
+
operation.text_widget.delete("1.0", tk.END)
|
|
558
|
+
operation.text_widget.insert("1.0", operation.processed_text)
|
|
559
|
+
operation.text_widget.config(state="disabled")
|
|
560
|
+
except tk.TclError:
|
|
561
|
+
pass
|
|
562
|
+
|
|
563
|
+
operation.text_widget.after_idle(update)
|
|
564
|
+
|
|
565
|
+
def _update_text_widget_partial(self, operation: FindReplaceOperation, text: str):
|
|
566
|
+
"""Update the text widget with partial processed text."""
|
|
567
|
+
def update():
|
|
568
|
+
try:
|
|
569
|
+
operation.text_widget.config(state="normal")
|
|
570
|
+
operation.text_widget.delete("1.0", tk.END)
|
|
571
|
+
operation.text_widget.insert("1.0", text)
|
|
572
|
+
operation.text_widget.config(state="disabled")
|
|
573
|
+
except tk.TclError:
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
operation.text_widget.after_idle(update)
|
|
577
|
+
|
|
578
|
+
def cancel_operation(self, operation_id: str) -> bool:
|
|
579
|
+
"""Cancel a running find/replace operation."""
|
|
580
|
+
with self.operation_lock:
|
|
581
|
+
if operation_id in self.active_operations:
|
|
582
|
+
operation = self.active_operations[operation_id]
|
|
583
|
+
operation.is_cancelled = True
|
|
584
|
+
self.performance_stats['cancelled_operations'] += 1
|
|
585
|
+
return True
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def cancel_all_operations(self):
|
|
589
|
+
"""Cancel all running find/replace operations."""
|
|
590
|
+
with self.operation_lock:
|
|
591
|
+
for operation in self.active_operations.values():
|
|
592
|
+
operation.is_cancelled = True
|
|
593
|
+
self.performance_stats['cancelled_operations'] += len(self.active_operations)
|
|
594
|
+
self.active_operations.clear()
|
|
595
|
+
|
|
596
|
+
def get_operation_status(self, operation_id: str) -> Optional[FindReplaceOperation]:
|
|
597
|
+
"""Get the status of a find/replace operation."""
|
|
598
|
+
with self.operation_lock:
|
|
599
|
+
return self.active_operations.get(operation_id)
|
|
600
|
+
|
|
601
|
+
def get_active_operations(self) -> List[str]:
|
|
602
|
+
"""Get list of active operation IDs."""
|
|
603
|
+
with self.operation_lock:
|
|
604
|
+
return list(self.active_operations.keys())
|
|
605
|
+
|
|
606
|
+
def get_performance_stats(self) -> Dict[str, Any]:
|
|
607
|
+
"""Get performance statistics."""
|
|
608
|
+
with self.operation_lock:
|
|
609
|
+
return self.performance_stats.copy()
|
|
610
|
+
|
|
611
|
+
def process_find_replace(self, text, find_pattern, replace_text, mode="Text", options=None):
|
|
612
|
+
"""
|
|
613
|
+
Process find and replace operation with compatibility for the main application.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
text: Input text to process
|
|
617
|
+
find_pattern: Pattern to search for
|
|
618
|
+
replace_text: Text to replace matches with
|
|
619
|
+
mode: "Text" or "Regex"
|
|
620
|
+
options: Dictionary with options like ignore_case, whole_words, etc.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Result object with processed_text, success, error_message, processing_time_ms
|
|
624
|
+
"""
|
|
625
|
+
from dataclasses import dataclass
|
|
626
|
+
import time
|
|
627
|
+
import re
|
|
628
|
+
|
|
629
|
+
@dataclass
|
|
630
|
+
class ProcessResult:
|
|
631
|
+
processed_text: str = ""
|
|
632
|
+
success: bool = True
|
|
633
|
+
error_message: str = ""
|
|
634
|
+
processing_time_ms: float = 0.0
|
|
635
|
+
|
|
636
|
+
start_time = time.time()
|
|
637
|
+
result = ProcessResult()
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
if not options:
|
|
641
|
+
options = {}
|
|
642
|
+
|
|
643
|
+
# Convert options to our format
|
|
644
|
+
case_sensitive = not options.get('ignore_case', False)
|
|
645
|
+
whole_words = options.get('whole_words', False)
|
|
646
|
+
use_regex = (mode == "Regex")
|
|
647
|
+
|
|
648
|
+
# Process directly without using the async worker
|
|
649
|
+
if use_regex:
|
|
650
|
+
# Handle regex mode
|
|
651
|
+
try:
|
|
652
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
653
|
+
result.processed_text = re.sub(find_pattern, replace_text, text, flags=flags)
|
|
654
|
+
result.success = True
|
|
655
|
+
except re.error as e:
|
|
656
|
+
result.success = False
|
|
657
|
+
result.error_message = f"Regex error: {e}"
|
|
658
|
+
result.processed_text = text
|
|
659
|
+
else:
|
|
660
|
+
# Handle text mode
|
|
661
|
+
if whole_words:
|
|
662
|
+
# Whole words matching
|
|
663
|
+
pattern = r'\b' + re.escape(find_pattern) + r'\b'
|
|
664
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
665
|
+
try:
|
|
666
|
+
result.processed_text = re.sub(pattern, replace_text, text, flags=flags)
|
|
667
|
+
result.success = True
|
|
668
|
+
except re.error as e:
|
|
669
|
+
result.success = False
|
|
670
|
+
result.error_message = f"Whole words error: {e}"
|
|
671
|
+
result.processed_text = text
|
|
672
|
+
else:
|
|
673
|
+
# Simple text replacement
|
|
674
|
+
if case_sensitive:
|
|
675
|
+
result.processed_text = text.replace(find_pattern, replace_text)
|
|
676
|
+
else:
|
|
677
|
+
# Case-insensitive replacement
|
|
678
|
+
pattern = re.escape(find_pattern)
|
|
679
|
+
result.processed_text = re.sub(pattern, replace_text, text, flags=re.IGNORECASE)
|
|
680
|
+
result.success = True
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
result.success = False
|
|
684
|
+
result.error_message = str(e)
|
|
685
|
+
result.processed_text = text
|
|
686
|
+
|
|
687
|
+
result.processing_time_ms = (time.time() - start_time) * 1000
|
|
688
|
+
return result
|
|
689
|
+
|
|
690
|
+
def shutdown(self):
|
|
691
|
+
"""Shutdown the processor and cleanup resources."""
|
|
692
|
+
self.cancel_all_operations()
|
|
693
|
+
self.shutdown_event.set()
|
|
694
|
+
|
|
695
|
+
if self.worker_thread and self.worker_thread.is_alive():
|
|
696
|
+
# Signal shutdown
|
|
697
|
+
self.operation_queue.put(None)
|
|
698
|
+
self.worker_thread.join(timeout=2.0)
|
|
699
|
+
|
|
700
|
+
# Global instance
|
|
701
|
+
_global_find_replace_processor = None
|
|
702
|
+
|
|
703
|
+
def get_find_replace_processor() -> OptimizedFindReplace:
|
|
704
|
+
"""Get the global find/replace processor instance."""
|
|
705
|
+
global _global_find_replace_processor
|
|
706
|
+
if _global_find_replace_processor is None:
|
|
707
|
+
_global_find_replace_processor = OptimizedFindReplace()
|
|
708
|
+
return _global_find_replace_processor
|
|
709
|
+
|
|
710
|
+
def shutdown_find_replace_processor():
|
|
711
|
+
"""Shutdown the global find/replace processor."""
|
|
712
|
+
global _global_find_replace_processor
|
|
713
|
+
if _global_find_replace_processor is not None:
|
|
714
|
+
_global_find_replace_processor.shutdown()
|
|
715
715
|
_global_find_replace_processor = None
|