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
package/tools/diff_viewer.py
CHANGED
|
@@ -1,1072 +1,1817 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Diff Viewer Tool Module
|
|
3
|
-
|
|
4
|
-
This module provides a comprehensive text comparison tool with multiple diff algorithms
|
|
5
|
-
and preprocessing options. It supports side-by-side comparison with synchronized scrolling
|
|
6
|
-
and word-level highlighting of differences.
|
|
7
|
-
|
|
8
|
-
Features:
|
|
9
|
-
- Multiple comparison modes (ignore case, match case, ignore whitespace)
|
|
10
|
-
- Side-by-side text comparison with synchronized scrolling
|
|
11
|
-
- Word-level difference highlighting
|
|
12
|
-
- Tab-based interface for multiple comparisons
|
|
13
|
-
- Integration with optimized text widgets when available
|
|
14
|
-
|
|
15
|
-
Author: Promera AI Commander
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import tkinter as tk
|
|
19
|
-
from tkinter import ttk
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.text.bind("
|
|
56
|
-
self._on_text_modified
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.
|
|
97
|
-
self.
|
|
98
|
-
self.
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
self.
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
ttk.Button(
|
|
202
|
-
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
ttk.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
self.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
self.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def
|
|
401
|
-
"""
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
self.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
self.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
except (tk.TclError, IndexError):
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
def
|
|
660
|
-
"""
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
Args:
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
self.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
try:
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
#
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
#
|
|
894
|
-
if
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1
|
+
"""
|
|
2
|
+
Diff Viewer Tool Module
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive text comparison tool with multiple diff algorithms
|
|
5
|
+
and preprocessing options. It supports side-by-side comparison with synchronized scrolling
|
|
6
|
+
and word-level highlighting of differences.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Multiple comparison modes (ignore case, match case, ignore whitespace)
|
|
10
|
+
- Side-by-side text comparison with synchronized scrolling
|
|
11
|
+
- Word-level difference highlighting
|
|
12
|
+
- Tab-based interface for multiple comparisons
|
|
13
|
+
- Integration with optimized text widgets when available
|
|
14
|
+
|
|
15
|
+
Author: Promera AI Commander
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import tkinter as tk
|
|
19
|
+
from tkinter import ttk, filedialog, messagebox
|
|
20
|
+
import difflib
|
|
21
|
+
import re
|
|
22
|
+
import platform
|
|
23
|
+
import logging
|
|
24
|
+
import subprocess
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Dict, Any, List, Optional
|
|
28
|
+
|
|
29
|
+
# Import optimized components when available
|
|
30
|
+
try:
|
|
31
|
+
from core.efficient_line_numbers import OptimizedTextWithLineNumbers
|
|
32
|
+
EFFICIENT_LINE_NUMBERS_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
EFFICIENT_LINE_NUMBERS_AVAILABLE = False
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from core.memory_efficient_text_widget import MemoryEfficientTextWidget
|
|
38
|
+
MEMORY_EFFICIENT_TEXT_AVAILABLE = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
MEMORY_EFFICIENT_TEXT_AVAILABLE = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TextWithLineNumbers(tk.Frame):
|
|
44
|
+
"""Fallback implementation of TextWithLineNumbers when optimized components are not available."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *args, **kwargs):
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
self.text = tk.Text(self, wrap=tk.WORD, height=15, width=50, undo=True)
|
|
49
|
+
self.linenumbers = tk.Canvas(self, width=50, bg='#f0f0f0', highlightthickness=0)
|
|
50
|
+
|
|
51
|
+
self.linenumbers.pack(side=tk.LEFT, fill=tk.Y)
|
|
52
|
+
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
53
|
+
|
|
54
|
+
# Basic event bindings
|
|
55
|
+
self.text.bind("<<Modified>>", self._on_text_modified)
|
|
56
|
+
self.text.bind("<Configure>", self._on_text_modified)
|
|
57
|
+
self._on_text_modified()
|
|
58
|
+
|
|
59
|
+
def _on_text_modified(self, event=None):
|
|
60
|
+
"""Update line numbers when text is modified."""
|
|
61
|
+
self.linenumbers.delete("all")
|
|
62
|
+
line_info_cache = []
|
|
63
|
+
i = self.text.index("@0,0")
|
|
64
|
+
while True:
|
|
65
|
+
dline = self.text.dlineinfo(i)
|
|
66
|
+
if dline is None:
|
|
67
|
+
break
|
|
68
|
+
line_info_cache.append((i, dline[1]))
|
|
69
|
+
i = self.text.index("%s+1line" % i)
|
|
70
|
+
|
|
71
|
+
for i, y in line_info_cache:
|
|
72
|
+
linenum = str(i).split(".")[0]
|
|
73
|
+
self.linenumbers.create_text(20, y, anchor="n", text=linenum, fill="gray")
|
|
74
|
+
|
|
75
|
+
if event and hasattr(event.widget, 'edit_modified') and event.widget.edit_modified():
|
|
76
|
+
event.widget.edit_modified(False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DiffViewerWidget:
|
|
80
|
+
"""
|
|
81
|
+
A comprehensive diff viewer widget that provides side-by-side text comparison
|
|
82
|
+
with multiple comparison algorithms and preprocessing options.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, parent, tab_count=7, logger=None, parent_callback=None, dialog_manager=None):
|
|
86
|
+
"""
|
|
87
|
+
Initialize the diff viewer widget.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
parent: Parent tkinter widget
|
|
91
|
+
tab_count: Number of tabs to create (default: 7)
|
|
92
|
+
logger: Logger instance for debugging
|
|
93
|
+
parent_callback: Callback function to notify parent of changes
|
|
94
|
+
dialog_manager: DialogManager instance for consistent dialog handling
|
|
95
|
+
"""
|
|
96
|
+
self.parent = parent
|
|
97
|
+
self.tab_count = tab_count
|
|
98
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
99
|
+
self.parent_callback = parent_callback
|
|
100
|
+
self.dialog_manager = dialog_manager
|
|
101
|
+
|
|
102
|
+
# Settings for diff comparison
|
|
103
|
+
self.settings = {
|
|
104
|
+
"option": "ignore_case", # Default comparison mode
|
|
105
|
+
"char_level_diff": False, # Character-level diff mode (vs word-level)
|
|
106
|
+
"detect_moved": False, # Detect moved lines
|
|
107
|
+
"syntax_highlight": False # Syntax highlighting for code
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Create the main frame
|
|
111
|
+
self.diff_frame = ttk.Frame(parent, padding="10")
|
|
112
|
+
self.diff_frame.grid_columnconfigure(0, weight=1)
|
|
113
|
+
self.diff_frame.grid_columnconfigure(1, weight=1)
|
|
114
|
+
self.diff_frame.grid_rowconfigure(1, weight=1)
|
|
115
|
+
|
|
116
|
+
# Statistics bars
|
|
117
|
+
self.input_stats_bar = None
|
|
118
|
+
self.output_stats_bar = None
|
|
119
|
+
|
|
120
|
+
# Diff tracking for navigation and summary
|
|
121
|
+
self.diff_positions = [] # List of (line_number, diff_type) tuples
|
|
122
|
+
self.current_diff_index = -1
|
|
123
|
+
self.diff_counts = {"additions": 0, "deletions": 0, "modifications": 0}
|
|
124
|
+
self.similarity_score = 0.0
|
|
125
|
+
self.diff_summary_bar = None
|
|
126
|
+
|
|
127
|
+
# Regex filter mode flags
|
|
128
|
+
self.input_regex_mode = tk.BooleanVar(value=False)
|
|
129
|
+
self.output_regex_mode = tk.BooleanVar(value=False)
|
|
130
|
+
|
|
131
|
+
# Store original source text for comparison (to avoid accumulating blank lines)
|
|
132
|
+
self.comparison_source_input = {} # {tab_idx: original_text}
|
|
133
|
+
self.comparison_source_output = {} # {tab_idx: original_text}
|
|
134
|
+
self.has_run_comparison = {} # {tab_idx: bool}
|
|
135
|
+
|
|
136
|
+
# Initialize UI components
|
|
137
|
+
self._create_ui()
|
|
138
|
+
self._setup_event_bindings()
|
|
139
|
+
|
|
140
|
+
def _show_error(self, title, message):
|
|
141
|
+
"""Show error dialog using DialogManager if available, otherwise use messagebox."""
|
|
142
|
+
if self.dialog_manager:
|
|
143
|
+
return self.dialog_manager.show_error(title, message, parent=self.parent)
|
|
144
|
+
else:
|
|
145
|
+
try:
|
|
146
|
+
from tkinter import messagebox
|
|
147
|
+
messagebox.showerror(title, message, parent=self.parent)
|
|
148
|
+
return True
|
|
149
|
+
except Exception:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def _show_warning(self, title, message, category="warning"):
|
|
153
|
+
"""Show warning dialog using DialogManager if available, otherwise use messagebox."""
|
|
154
|
+
if self.dialog_manager:
|
|
155
|
+
return self.dialog_manager.show_warning(title, message, category, parent=self.parent)
|
|
156
|
+
else:
|
|
157
|
+
try:
|
|
158
|
+
import tkinter.messagebox as messagebox
|
|
159
|
+
messagebox.showwarning(title, message, parent=self.parent)
|
|
160
|
+
return True
|
|
161
|
+
except Exception:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def _create_ui(self):
|
|
165
|
+
"""Create the user interface components."""
|
|
166
|
+
self._create_title_rows()
|
|
167
|
+
self._create_notebooks()
|
|
168
|
+
self._create_tabs()
|
|
169
|
+
self._create_statistics_bars()
|
|
170
|
+
self._create_diff_summary_bar()
|
|
171
|
+
self._configure_text_tags()
|
|
172
|
+
|
|
173
|
+
def _create_title_rows(self):
|
|
174
|
+
"""Create the title rows with buttons and controls."""
|
|
175
|
+
# Input title row
|
|
176
|
+
input_title_row = ttk.Frame(self.diff_frame)
|
|
177
|
+
input_title_row.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
|
178
|
+
|
|
179
|
+
# Input label and buttons
|
|
180
|
+
input_controls = ttk.Frame(input_title_row)
|
|
181
|
+
input_controls.pack(side=tk.TOP, fill=tk.X)
|
|
182
|
+
ttk.Label(input_controls, text="Input", font=("Helvetica", 12, "bold")).pack(side=tk.LEFT)
|
|
183
|
+
|
|
184
|
+
# Load from file button
|
|
185
|
+
load_file_btn = ttk.Button(input_controls, text="📁", command=self.load_file_to_input, width=3)
|
|
186
|
+
load_file_btn.pack(side=tk.LEFT, padx=(10, 0))
|
|
187
|
+
|
|
188
|
+
# Erase button
|
|
189
|
+
ttk.Button(input_controls, text="⌫", command=self.clear_all_input_tabs, width=3).pack(side=tk.LEFT, padx=(5, 0))
|
|
190
|
+
|
|
191
|
+
# Input line filter
|
|
192
|
+
input_filter_frame = ttk.Frame(input_title_row)
|
|
193
|
+
input_filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
|
|
194
|
+
ttk.Label(input_filter_frame, text="Filter:").pack(side=tk.LEFT)
|
|
195
|
+
self.input_filter_var = tk.StringVar()
|
|
196
|
+
self.input_filter_entry = ttk.Entry(input_filter_frame, textvariable=self.input_filter_var, width=25)
|
|
197
|
+
self.input_filter_entry.pack(side=tk.LEFT, padx=(5, 2), fill=tk.X, expand=True)
|
|
198
|
+
self.input_filter_var.trace_add("write", self._on_input_filter_changed)
|
|
199
|
+
ttk.Checkbutton(input_filter_frame, text="Rx", variable=self.input_regex_mode,
|
|
200
|
+
command=self._on_input_filter_changed, width=3).pack(side=tk.LEFT, padx=(0, 2))
|
|
201
|
+
ttk.Button(input_filter_frame, text="✕", command=self._clear_input_filter, width=3).pack(side=tk.LEFT)
|
|
202
|
+
|
|
203
|
+
# Output title row
|
|
204
|
+
output_title_row = ttk.Frame(self.diff_frame)
|
|
205
|
+
output_title_row.grid(row=0, column=1, sticky="ew", padx=(5, 0))
|
|
206
|
+
|
|
207
|
+
# Output label and buttons
|
|
208
|
+
output_controls = ttk.Frame(output_title_row)
|
|
209
|
+
output_controls.pack(side=tk.TOP, fill=tk.X)
|
|
210
|
+
ttk.Label(output_controls, text="Output", font=("Helvetica", 12, "bold")).pack(side=tk.LEFT)
|
|
211
|
+
|
|
212
|
+
# Send to Input dropdown
|
|
213
|
+
self.send_to_input_var = tk.StringVar(value="Send to Input")
|
|
214
|
+
send_to_input_menu = ttk.Menubutton(output_controls, textvariable=self.send_to_input_var, direction="below")
|
|
215
|
+
send_to_input_menu.pack(side=tk.LEFT, padx=(10, 6))
|
|
216
|
+
|
|
217
|
+
# Create the dropdown menu
|
|
218
|
+
dropdown_menu = tk.Menu(send_to_input_menu, tearoff=0)
|
|
219
|
+
send_to_input_menu.config(menu=dropdown_menu)
|
|
220
|
+
for i in range(self.tab_count):
|
|
221
|
+
dropdown_menu.add_command(label=f"Tab {i+1}", command=lambda tab=i: self.copy_to_specific_input_tab(tab))
|
|
222
|
+
|
|
223
|
+
# Copy to clipboard button
|
|
224
|
+
ttk.Button(output_controls, text="⎘", command=self.copy_to_clipboard, width=3).pack(side=tk.LEFT, padx=(0, 6))
|
|
225
|
+
|
|
226
|
+
# Erase button
|
|
227
|
+
ttk.Button(output_controls, text="⌫", command=self.clear_all_output_tabs, width=3).pack(side=tk.LEFT)
|
|
228
|
+
|
|
229
|
+
# Output line filter
|
|
230
|
+
output_filter_frame = ttk.Frame(output_title_row)
|
|
231
|
+
output_filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
|
|
232
|
+
ttk.Label(output_filter_frame, text="Filter:").pack(side=tk.LEFT)
|
|
233
|
+
self.output_filter_var = tk.StringVar()
|
|
234
|
+
self.output_filter_entry = ttk.Entry(output_filter_frame, textvariable=self.output_filter_var, width=25)
|
|
235
|
+
self.output_filter_entry.pack(side=tk.LEFT, padx=(5, 2), fill=tk.X, expand=True)
|
|
236
|
+
self.output_filter_var.trace_add("write", self._on_output_filter_changed)
|
|
237
|
+
ttk.Checkbutton(output_filter_frame, text="Rx", variable=self.output_regex_mode,
|
|
238
|
+
command=self._on_output_filter_changed, width=3).pack(side=tk.LEFT, padx=(0, 2))
|
|
239
|
+
ttk.Button(output_filter_frame, text="✕", command=self._clear_output_filter, width=3).pack(side=tk.LEFT)
|
|
240
|
+
|
|
241
|
+
# Store original content for filtering
|
|
242
|
+
self.input_original_content = {}
|
|
243
|
+
self.output_original_content = {}
|
|
244
|
+
|
|
245
|
+
def _create_notebooks(self):
|
|
246
|
+
"""Create the notebook widgets for input and output tabs."""
|
|
247
|
+
self.input_notebook = ttk.Notebook(self.diff_frame)
|
|
248
|
+
self.input_notebook.grid(row=1, column=0, sticky="nsew", padx=(0, 5))
|
|
249
|
+
|
|
250
|
+
self.output_notebook = ttk.Notebook(self.diff_frame)
|
|
251
|
+
self.output_notebook.grid(row=1, column=1, sticky="nsew", padx=(5, 0))
|
|
252
|
+
|
|
253
|
+
def _create_statistics_bars(self):
|
|
254
|
+
"""Create statistics bars below the text areas."""
|
|
255
|
+
# Input statistics bar
|
|
256
|
+
self.input_stats_bar = ttk.Label(
|
|
257
|
+
self.diff_frame,
|
|
258
|
+
text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0",
|
|
259
|
+
relief=tk.SUNKEN,
|
|
260
|
+
anchor=tk.W,
|
|
261
|
+
padding=(5, 2)
|
|
262
|
+
)
|
|
263
|
+
self.input_stats_bar.grid(row=2, column=0, sticky="ew", padx=(0, 5), pady=(5, 0))
|
|
264
|
+
|
|
265
|
+
# Output statistics bar
|
|
266
|
+
self.output_stats_bar = ttk.Label(
|
|
267
|
+
self.diff_frame,
|
|
268
|
+
text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0",
|
|
269
|
+
relief=tk.SUNKEN,
|
|
270
|
+
anchor=tk.W,
|
|
271
|
+
padding=(5, 2)
|
|
272
|
+
)
|
|
273
|
+
self.output_stats_bar.grid(row=2, column=1, sticky="ew", padx=(5, 0), pady=(5, 0))
|
|
274
|
+
|
|
275
|
+
def _create_diff_summary_bar(self):
|
|
276
|
+
"""Create diff summary bar with navigation buttons and similarity score."""
|
|
277
|
+
# Container frame spanning both columns
|
|
278
|
+
summary_frame = ttk.Frame(self.diff_frame)
|
|
279
|
+
summary_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(5, 0))
|
|
280
|
+
|
|
281
|
+
# Navigation buttons on the left
|
|
282
|
+
nav_frame = ttk.Frame(summary_frame)
|
|
283
|
+
nav_frame.pack(side=tk.LEFT)
|
|
284
|
+
|
|
285
|
+
self.prev_diff_btn = ttk.Button(nav_frame, text="⬆ Prev", command=self._goto_prev_diff, width=8)
|
|
286
|
+
self.prev_diff_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
287
|
+
self.prev_diff_btn.state(['disabled'])
|
|
288
|
+
|
|
289
|
+
self.next_diff_btn = ttk.Button(nav_frame, text="⬇ Next", command=self._goto_next_diff, width=8)
|
|
290
|
+
self.next_diff_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
291
|
+
self.next_diff_btn.state(['disabled'])
|
|
292
|
+
|
|
293
|
+
# Diff summary label in the center
|
|
294
|
+
self.diff_summary_bar = ttk.Label(
|
|
295
|
+
summary_frame,
|
|
296
|
+
text="Run comparison to see diff summary",
|
|
297
|
+
anchor=tk.CENTER,
|
|
298
|
+
padding=(5, 2)
|
|
299
|
+
)
|
|
300
|
+
self.diff_summary_bar.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
301
|
+
|
|
302
|
+
# Export button on the right
|
|
303
|
+
self.export_html_btn = ttk.Button(summary_frame, text="Export HTML", command=self._export_to_html, width=13)
|
|
304
|
+
self.export_html_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
|
305
|
+
|
|
306
|
+
def _create_tabs(self):
|
|
307
|
+
"""Create the text tabs for input and output."""
|
|
308
|
+
self.input_tabs = []
|
|
309
|
+
self.output_tabs = []
|
|
310
|
+
|
|
311
|
+
for i in range(self.tab_count):
|
|
312
|
+
# Create input tab
|
|
313
|
+
if EFFICIENT_LINE_NUMBERS_AVAILABLE:
|
|
314
|
+
input_tab = OptimizedTextWithLineNumbers(self.input_notebook)
|
|
315
|
+
elif MEMORY_EFFICIENT_TEXT_AVAILABLE:
|
|
316
|
+
input_tab = MemoryEfficientTextWidget(self.input_notebook)
|
|
317
|
+
else:
|
|
318
|
+
input_tab = TextWithLineNumbers(self.input_notebook)
|
|
319
|
+
|
|
320
|
+
input_tab.text.bind("<<Modified>>", self._on_tab_content_changed)
|
|
321
|
+
input_tab.text.bind("<KeyRelease>", self._on_tab_content_changed)
|
|
322
|
+
input_tab.text.bind("<Button-1>", self._on_tab_content_changed)
|
|
323
|
+
self.input_tabs.append(input_tab)
|
|
324
|
+
self.input_notebook.add(input_tab, text=f"{i+1}:")
|
|
325
|
+
|
|
326
|
+
# Create output tab
|
|
327
|
+
if EFFICIENT_LINE_NUMBERS_AVAILABLE:
|
|
328
|
+
output_tab = OptimizedTextWithLineNumbers(self.output_notebook)
|
|
329
|
+
elif MEMORY_EFFICIENT_TEXT_AVAILABLE:
|
|
330
|
+
output_tab = MemoryEfficientTextWidget(self.output_notebook)
|
|
331
|
+
else:
|
|
332
|
+
output_tab = TextWithLineNumbers(self.output_notebook)
|
|
333
|
+
|
|
334
|
+
output_tab.text.bind("<<Modified>>", self._on_tab_content_changed)
|
|
335
|
+
output_tab.text.bind("<KeyRelease>", self._on_tab_content_changed)
|
|
336
|
+
output_tab.text.bind("<Button-1>", self._on_tab_content_changed)
|
|
337
|
+
self.output_tabs.append(output_tab)
|
|
338
|
+
self.output_notebook.add(output_tab, text=f"{i+1}:")
|
|
339
|
+
|
|
340
|
+
def _configure_text_tags(self):
|
|
341
|
+
"""Configure text tags for highlighting differences and syntax."""
|
|
342
|
+
for tab_list in [self.input_tabs, self.output_tabs]:
|
|
343
|
+
for tab in tab_list:
|
|
344
|
+
widget = tab.text
|
|
345
|
+
widget.config(state="normal")
|
|
346
|
+
# Diff highlighting tags
|
|
347
|
+
widget.tag_configure("addition", background="#e6ffed")
|
|
348
|
+
widget.tag_configure("deletion", background="#ffebe9")
|
|
349
|
+
widget.tag_configure("modification", background="#e6f7ff")
|
|
350
|
+
widget.tag_configure("inline_add", background="#a7f0ba")
|
|
351
|
+
widget.tag_configure("inline_del", background="#ffc9c9")
|
|
352
|
+
widget.tag_configure("moved", background="#f3e8ff") # Lavender for moved lines
|
|
353
|
+
|
|
354
|
+
# Syntax highlighting tags (lower priority than diff tags)
|
|
355
|
+
widget.tag_configure("syntax_keyword", foreground="#0000ff") # Blue
|
|
356
|
+
widget.tag_configure("syntax_string", foreground="#008000") # Green
|
|
357
|
+
widget.tag_configure("syntax_comment", foreground="#808080", font=("", 0, "italic")) # Gray italic
|
|
358
|
+
widget.tag_configure("syntax_number", foreground="#ff8c00") # Dark orange
|
|
359
|
+
widget.tag_configure("syntax_function", foreground="#800080") # Purple
|
|
360
|
+
widget.tag_configure("syntax_decorator", foreground="#b8860b") # Dark goldenrod
|
|
361
|
+
widget.tag_configure("syntax_class", foreground="#2e8b57") # Sea green
|
|
362
|
+
|
|
363
|
+
def _setup_event_bindings(self):
|
|
364
|
+
"""Set up event bindings for synchronized scrolling."""
|
|
365
|
+
self.input_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
|
366
|
+
self.output_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
|
367
|
+
self._setup_sync()
|
|
368
|
+
|
|
369
|
+
def _on_tab_changed(self, event=None):
|
|
370
|
+
"""Handle tab change events."""
|
|
371
|
+
# Clear filters when switching tabs
|
|
372
|
+
if hasattr(self, 'input_filter_var'):
|
|
373
|
+
self.input_filter_var.set("")
|
|
374
|
+
if hasattr(self, 'output_filter_var'):
|
|
375
|
+
self.output_filter_var.set("")
|
|
376
|
+
|
|
377
|
+
self._setup_sync(event)
|
|
378
|
+
self.update_statistics()
|
|
379
|
+
|
|
380
|
+
def _setup_sync(self, event=None):
|
|
381
|
+
"""Configure scroll and mousewheel syncing for the active tabs."""
|
|
382
|
+
try:
|
|
383
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
384
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
385
|
+
except (tk.TclError, IndexError):
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
# Configure scrollbar syncing
|
|
389
|
+
if hasattr(active_input_tab.text, 'vbar'):
|
|
390
|
+
active_input_tab.text.vbar.config(command=self._sync_scroll)
|
|
391
|
+
if hasattr(active_output_tab.text, 'vbar'):
|
|
392
|
+
active_output_tab.text.vbar.config(command=self._sync_scroll)
|
|
393
|
+
|
|
394
|
+
# Configure mouse wheel syncing
|
|
395
|
+
for tab in [active_input_tab, active_output_tab]:
|
|
396
|
+
tab.text.bind("<MouseWheel>", self._on_mousewheel)
|
|
397
|
+
tab.text.bind("<Button-4>", self._on_mousewheel)
|
|
398
|
+
tab.text.bind("<Button-5>", self._on_mousewheel)
|
|
399
|
+
|
|
400
|
+
def _sync_scroll(self, *args):
|
|
401
|
+
"""Sync both text widgets when one's scrollbar is used."""
|
|
402
|
+
try:
|
|
403
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
404
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
405
|
+
|
|
406
|
+
active_input_tab.text.yview(*args)
|
|
407
|
+
active_output_tab.text.yview(*args)
|
|
408
|
+
|
|
409
|
+
# Update line numbers if available
|
|
410
|
+
if hasattr(active_input_tab, '_on_text_modified'):
|
|
411
|
+
active_input_tab._on_text_modified()
|
|
412
|
+
if hasattr(active_output_tab, '_on_text_modified'):
|
|
413
|
+
active_output_tab._on_text_modified()
|
|
414
|
+
except (tk.TclError, IndexError):
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
def _on_mousewheel(self, event):
|
|
418
|
+
"""Handle mouse wheel scrolling over either text widget."""
|
|
419
|
+
if platform.system() == "Windows":
|
|
420
|
+
delta = int(-1*(event.delta/120))
|
|
421
|
+
elif platform.system() == "Darwin":
|
|
422
|
+
delta = int(-1 * event.delta)
|
|
423
|
+
else:
|
|
424
|
+
delta = -1 if event.num == 4 else 1
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
428
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
429
|
+
|
|
430
|
+
active_input_tab.text.yview_scroll(delta, "units")
|
|
431
|
+
active_output_tab.text.yview_scroll(delta, "units")
|
|
432
|
+
|
|
433
|
+
# Update line numbers if available
|
|
434
|
+
if hasattr(active_input_tab, '_on_text_modified'):
|
|
435
|
+
active_input_tab._on_text_modified()
|
|
436
|
+
if hasattr(active_output_tab, '_on_text_modified'):
|
|
437
|
+
active_output_tab._on_text_modified()
|
|
438
|
+
except (tk.TclError, IndexError):
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
return "break"
|
|
442
|
+
|
|
443
|
+
def _on_tab_content_changed(self, event=None):
|
|
444
|
+
"""Handle tab content changes."""
|
|
445
|
+
# Update tab labels when content changes
|
|
446
|
+
self.update_tab_labels()
|
|
447
|
+
|
|
448
|
+
# Update statistics
|
|
449
|
+
self.update_statistics()
|
|
450
|
+
|
|
451
|
+
# This can be overridden by the parent application
|
|
452
|
+
if hasattr(self, 'parent_callback') and self.parent_callback:
|
|
453
|
+
self.parent_callback()
|
|
454
|
+
|
|
455
|
+
def get_frame(self):
|
|
456
|
+
"""Return the main frame widget."""
|
|
457
|
+
return self.diff_frame
|
|
458
|
+
|
|
459
|
+
def show(self):
|
|
460
|
+
"""Show the diff viewer."""
|
|
461
|
+
self.diff_frame.grid(row=0, column=0, sticky="nsew", pady=5)
|
|
462
|
+
|
|
463
|
+
def hide(self):
|
|
464
|
+
"""Hide the diff viewer."""
|
|
465
|
+
self.diff_frame.grid_remove()
|
|
466
|
+
|
|
467
|
+
def load_content(self, input_tabs_content, output_tabs_content):
|
|
468
|
+
"""
|
|
469
|
+
Load content into the diff viewer tabs.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
input_tabs_content: List of strings for input tabs
|
|
473
|
+
output_tabs_content: List of strings for output tabs
|
|
474
|
+
"""
|
|
475
|
+
self.logger.info("Loading content into Diff Viewer.")
|
|
476
|
+
|
|
477
|
+
for i in range(min(len(input_tabs_content), self.tab_count)):
|
|
478
|
+
self.input_tabs[i].text.delete("1.0", tk.END)
|
|
479
|
+
self.input_tabs[i].text.insert("1.0", input_tabs_content[i])
|
|
480
|
+
|
|
481
|
+
for i in range(min(len(output_tabs_content), self.tab_count)):
|
|
482
|
+
self.output_tabs[i].text.delete("1.0", tk.END)
|
|
483
|
+
self.output_tabs[i].text.insert("1.0", output_tabs_content[i])
|
|
484
|
+
|
|
485
|
+
# Clear stored comparison source text (so new content is compared fresh)
|
|
486
|
+
self.comparison_source_input.clear()
|
|
487
|
+
self.comparison_source_output.clear()
|
|
488
|
+
self.has_run_comparison.clear()
|
|
489
|
+
|
|
490
|
+
# Update tab labels after loading content
|
|
491
|
+
self.update_tab_labels()
|
|
492
|
+
|
|
493
|
+
def sync_content_back(self):
|
|
494
|
+
"""
|
|
495
|
+
Get content from diff viewer tabs.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
tuple: (input_contents, output_contents) as lists of strings
|
|
499
|
+
"""
|
|
500
|
+
self.logger.info("Syncing Diff Viewer content back.")
|
|
501
|
+
|
|
502
|
+
input_contents = []
|
|
503
|
+
output_contents = []
|
|
504
|
+
|
|
505
|
+
for i in range(self.tab_count):
|
|
506
|
+
input_content = self.input_tabs[i].text.get("1.0", tk.END)
|
|
507
|
+
# Remove trailing newline that tkinter adds
|
|
508
|
+
if input_content.endswith('\n'):
|
|
509
|
+
input_content = input_content[:-1]
|
|
510
|
+
input_contents.append(input_content)
|
|
511
|
+
|
|
512
|
+
output_content = self.output_tabs[i].text.get("1.0", tk.END)
|
|
513
|
+
# Remove trailing newline that tkinter adds
|
|
514
|
+
if output_content.endswith('\n'):
|
|
515
|
+
output_content = output_content[:-1]
|
|
516
|
+
output_contents.append(output_content)
|
|
517
|
+
|
|
518
|
+
# Debug logging
|
|
519
|
+
non_empty_inputs = sum(1 for content in input_contents if content.strip())
|
|
520
|
+
non_empty_outputs = sum(1 for content in output_contents if content.strip())
|
|
521
|
+
self.logger.info(f"Syncing back {non_empty_inputs} non-empty input tabs, {non_empty_outputs} non-empty output tabs")
|
|
522
|
+
|
|
523
|
+
return input_contents, output_contents
|
|
524
|
+
|
|
525
|
+
def _preprocess_for_diff(self, text, option):
|
|
526
|
+
"""
|
|
527
|
+
Preprocess text into line dicts according to diff option.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
text: Input text to preprocess
|
|
531
|
+
option: Comparison option ('ignore_case', 'match_case', 'ignore_whitespace', 'sentence_level')
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
List of dicts with 'raw' and 'cmp' keys
|
|
535
|
+
"""
|
|
536
|
+
# For sentence-level comparison, split by sentences instead of lines
|
|
537
|
+
if option == "sentence_level":
|
|
538
|
+
return self._preprocess_sentences(text)
|
|
539
|
+
|
|
540
|
+
lines = text.splitlines()
|
|
541
|
+
processed = []
|
|
542
|
+
for line in lines:
|
|
543
|
+
cmp_line = line
|
|
544
|
+
if option == "ignore_case":
|
|
545
|
+
cmp_line = cmp_line.lower()
|
|
546
|
+
elif option == "ignore_whitespace":
|
|
547
|
+
cmp_line = re.sub(r"\s+", " ", cmp_line).strip()
|
|
548
|
+
elif option == "ignore_punctuation":
|
|
549
|
+
# Remove all punctuation for comparison (useful for prose)
|
|
550
|
+
cmp_line = re.sub(r'[^\w\s]', '', cmp_line.lower()).strip()
|
|
551
|
+
cmp_line = re.sub(r"\s+", " ", cmp_line) # Normalize whitespace too
|
|
552
|
+
processed.append({"raw": line, "cmp": cmp_line})
|
|
553
|
+
return processed
|
|
554
|
+
|
|
555
|
+
def _preprocess_sentences(self, text):
|
|
556
|
+
"""
|
|
557
|
+
Split text into sentences for sentence-level comparison.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
text: Input text to split into sentences
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
List of dicts with 'raw' (original sentence) and 'cmp' (normalized) keys
|
|
564
|
+
"""
|
|
565
|
+
# Replace line breaks with spaces for continuous text
|
|
566
|
+
continuous_text = re.sub(r'\s+', ' ', text).strip()
|
|
567
|
+
|
|
568
|
+
if not continuous_text:
|
|
569
|
+
return []
|
|
570
|
+
|
|
571
|
+
# Split into sentences using pattern that handles common cases
|
|
572
|
+
# Matches: . ! ? followed by space or end of string
|
|
573
|
+
# But not: abbreviations like Mr. Mrs. Dr. etc.
|
|
574
|
+
sentence_pattern = r'(?<![A-Z][a-z])(?<![A-Z])(?<=\.|\!|\?)\s+'
|
|
575
|
+
sentences = re.split(sentence_pattern, continuous_text)
|
|
576
|
+
|
|
577
|
+
processed = []
|
|
578
|
+
for sentence in sentences:
|
|
579
|
+
sentence = sentence.strip()
|
|
580
|
+
if sentence:
|
|
581
|
+
# Normalize for comparison: lowercase, normalize whitespace
|
|
582
|
+
cmp_sentence = re.sub(r'\s+', ' ', sentence.lower()).strip()
|
|
583
|
+
processed.append({"raw": sentence, "cmp": cmp_sentence})
|
|
584
|
+
|
|
585
|
+
return processed
|
|
586
|
+
|
|
587
|
+
def _clean_alignment_blanks(self, text):
|
|
588
|
+
"""
|
|
589
|
+
Remove consecutive empty lines (alignment artifacts) while preserving single empty lines.
|
|
590
|
+
|
|
591
|
+
This prevents blank lines from accumulating across comparison sessions.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
text: Input text that may contain alignment blank lines
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Cleaned text with consecutive empty lines collapsed
|
|
598
|
+
"""
|
|
599
|
+
if not text:
|
|
600
|
+
return text
|
|
601
|
+
|
|
602
|
+
lines = text.splitlines()
|
|
603
|
+
cleaned_lines = []
|
|
604
|
+
prev_was_empty = False
|
|
605
|
+
|
|
606
|
+
for line in lines:
|
|
607
|
+
is_empty = line.strip() == ""
|
|
608
|
+
|
|
609
|
+
if is_empty and prev_was_empty:
|
|
610
|
+
# Skip consecutive empty lines
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
cleaned_lines.append(line)
|
|
614
|
+
prev_was_empty = is_empty
|
|
615
|
+
|
|
616
|
+
return '\n'.join(cleaned_lines)
|
|
617
|
+
|
|
618
|
+
def reset_comparison_source(self):
|
|
619
|
+
"""
|
|
620
|
+
Reset comparison source cache and clean widget content of accumulated blanks.
|
|
621
|
+
|
|
622
|
+
Called when user clicks 'Compare Active Tabs' to ensure fresh content is used.
|
|
623
|
+
"""
|
|
624
|
+
# Clear the source cache
|
|
625
|
+
self.comparison_source_input.clear()
|
|
626
|
+
self.comparison_source_output.clear()
|
|
627
|
+
self.has_run_comparison.clear()
|
|
628
|
+
|
|
629
|
+
# Clean the active tab widgets of accumulated blank lines
|
|
630
|
+
try:
|
|
631
|
+
active_input_idx = self.input_notebook.index("current")
|
|
632
|
+
active_output_idx = self.output_notebook.index("current")
|
|
633
|
+
|
|
634
|
+
input_widget = self.input_tabs[active_input_idx].text
|
|
635
|
+
output_widget = self.output_tabs[active_output_idx].text
|
|
636
|
+
|
|
637
|
+
# Get and clean input content
|
|
638
|
+
input_text = input_widget.get("1.0", tk.END)
|
|
639
|
+
if input_text.endswith('\n'):
|
|
640
|
+
input_text = input_text[:-1]
|
|
641
|
+
cleaned_input = self._clean_alignment_blanks(input_text)
|
|
642
|
+
|
|
643
|
+
# Get and clean output content
|
|
644
|
+
output_text = output_widget.get("1.0", tk.END)
|
|
645
|
+
if output_text.endswith('\n'):
|
|
646
|
+
output_text = output_text[:-1]
|
|
647
|
+
cleaned_output = self._clean_alignment_blanks(output_text)
|
|
648
|
+
|
|
649
|
+
# Replace widget content with cleaned version
|
|
650
|
+
input_widget.delete("1.0", tk.END)
|
|
651
|
+
input_widget.insert("1.0", cleaned_input)
|
|
652
|
+
|
|
653
|
+
output_widget.delete("1.0", tk.END)
|
|
654
|
+
output_widget.insert("1.0", cleaned_output)
|
|
655
|
+
|
|
656
|
+
except (tk.TclError, IndexError) as e:
|
|
657
|
+
self.logger.warning(f"Could not clean widget content: {e}")
|
|
658
|
+
|
|
659
|
+
def run_comparison(self, option=None):
|
|
660
|
+
"""
|
|
661
|
+
Compare the active tabs and display the diff.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
option: Comparison option ('ignore_case', 'match_case', 'ignore_whitespace')
|
|
665
|
+
If None, uses the current setting
|
|
666
|
+
"""
|
|
667
|
+
self.logger.info("Running Diff Viewer comparison.")
|
|
668
|
+
|
|
669
|
+
if option is not None:
|
|
670
|
+
self.settings["option"] = option
|
|
671
|
+
|
|
672
|
+
current_option = self.settings.get("option", "ignore_case")
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
active_input_idx = self.input_notebook.index("current")
|
|
676
|
+
active_output_idx = self.output_notebook.index("current")
|
|
677
|
+
|
|
678
|
+
input_widget = self.input_tabs[active_input_idx].text
|
|
679
|
+
output_widget = self.output_tabs[active_output_idx].text
|
|
680
|
+
|
|
681
|
+
# Check if we have stored original text from a previous comparison
|
|
682
|
+
# This prevents accumulating blank lines when switching comparison modes
|
|
683
|
+
tab_key = (active_input_idx, active_output_idx)
|
|
684
|
+
|
|
685
|
+
if tab_key in self.has_run_comparison and self.has_run_comparison[tab_key]:
|
|
686
|
+
# Use stored original text (without alignment blanks)
|
|
687
|
+
input_text = self.comparison_source_input.get(active_input_idx, "")
|
|
688
|
+
output_text = self.comparison_source_output.get(active_output_idx, "")
|
|
689
|
+
else:
|
|
690
|
+
# First comparison - read current widget content and store it
|
|
691
|
+
input_text = input_widget.get("1.0", tk.END)
|
|
692
|
+
if input_text.endswith('\n'):
|
|
693
|
+
input_text = input_text[:-1]
|
|
694
|
+
|
|
695
|
+
output_text = output_widget.get("1.0", tk.END)
|
|
696
|
+
if output_text.endswith('\n'):
|
|
697
|
+
output_text = output_text[:-1]
|
|
698
|
+
|
|
699
|
+
# Store the original source text (no blank line cleaning - preserves structure)
|
|
700
|
+
self.comparison_source_input[active_input_idx] = input_text
|
|
701
|
+
self.comparison_source_output[active_output_idx] = output_text
|
|
702
|
+
self.has_run_comparison[tab_key] = True
|
|
703
|
+
|
|
704
|
+
except (tk.TclError, IndexError):
|
|
705
|
+
self.logger.error("Could not get active tabs for comparison")
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
# Clear filters before comparison
|
|
709
|
+
if hasattr(self, 'input_filter_var'):
|
|
710
|
+
self.input_filter_var.set("")
|
|
711
|
+
if hasattr(self, 'output_filter_var'):
|
|
712
|
+
self.output_filter_var.set("")
|
|
713
|
+
|
|
714
|
+
# Clear stored original content
|
|
715
|
+
if active_input_idx in self.input_original_content:
|
|
716
|
+
del self.input_original_content[active_input_idx]
|
|
717
|
+
if active_output_idx in self.output_original_content:
|
|
718
|
+
del self.output_original_content[active_output_idx]
|
|
719
|
+
|
|
720
|
+
# Clear existing content
|
|
721
|
+
input_widget.delete("1.0", tk.END)
|
|
722
|
+
output_widget.delete("1.0", tk.END)
|
|
723
|
+
|
|
724
|
+
# Reset diff tracking
|
|
725
|
+
self.diff_positions = []
|
|
726
|
+
self.current_diff_index = -1
|
|
727
|
+
self.diff_counts = {"additions": 0, "deletions": 0, "modifications": 0}
|
|
728
|
+
self.similarity_score = 0.0
|
|
729
|
+
|
|
730
|
+
# Handle empty texts
|
|
731
|
+
if not input_text.strip() and not output_text.strip():
|
|
732
|
+
self._update_diff_summary()
|
|
733
|
+
return
|
|
734
|
+
elif not input_text.strip():
|
|
735
|
+
for i, line in enumerate(output_text.splitlines()):
|
|
736
|
+
input_widget.insert(tk.END, '\n')
|
|
737
|
+
output_widget.insert(tk.END, line + '\n', 'addition')
|
|
738
|
+
self.diff_positions.append((i + 1, 'addition'))
|
|
739
|
+
self.diff_counts["additions"] += 1
|
|
740
|
+
self._update_diff_summary()
|
|
741
|
+
return
|
|
742
|
+
elif not output_text.strip():
|
|
743
|
+
for i, line in enumerate(input_text.splitlines()):
|
|
744
|
+
input_widget.insert(tk.END, line + '\n', 'deletion')
|
|
745
|
+
output_widget.insert(tk.END, '\n')
|
|
746
|
+
self.diff_positions.append((i + 1, 'deletion'))
|
|
747
|
+
self.diff_counts["deletions"] += 1
|
|
748
|
+
self._update_diff_summary()
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Preprocess texts for comparison
|
|
752
|
+
left_lines = self._preprocess_for_diff(input_text, current_option)
|
|
753
|
+
right_lines = self._preprocess_for_diff(output_text, current_option)
|
|
754
|
+
left_cmp = [l["cmp"] for l in left_lines]
|
|
755
|
+
right_cmp = [r["cmp"] for r in right_lines]
|
|
756
|
+
|
|
757
|
+
try:
|
|
758
|
+
matcher = difflib.SequenceMatcher(None, left_cmp, right_cmp, autojunk=False)
|
|
759
|
+
|
|
760
|
+
# Compute similarity score
|
|
761
|
+
self.similarity_score = matcher.ratio() * 100
|
|
762
|
+
|
|
763
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
764
|
+
if tag == 'equal':
|
|
765
|
+
for i in range(i1, i2):
|
|
766
|
+
input_widget.insert(tk.END, left_lines[i]["raw"] + '\n')
|
|
767
|
+
output_widget.insert(tk.END, right_lines[j1 + (i - i1)]["raw"] + '\n')
|
|
768
|
+
|
|
769
|
+
elif tag == 'delete':
|
|
770
|
+
for i in range(i1, i2):
|
|
771
|
+
input_widget.insert(tk.END, left_lines[i]["raw"] + '\n', 'deletion')
|
|
772
|
+
output_widget.insert(tk.END, '\n')
|
|
773
|
+
# Get line number AFTER inserting (current line minus 1 since cursor is on next line)
|
|
774
|
+
actual_line = int(input_widget.index(tk.INSERT).split('.')[0]) - 1
|
|
775
|
+
if actual_line < 1:
|
|
776
|
+
actual_line = 1
|
|
777
|
+
self.diff_positions.append((actual_line, 'deletion'))
|
|
778
|
+
self.diff_counts["deletions"] += 1
|
|
779
|
+
|
|
780
|
+
elif tag == 'insert':
|
|
781
|
+
for j in range(j1, j2):
|
|
782
|
+
input_widget.insert(tk.END, '\n')
|
|
783
|
+
output_widget.insert(tk.END, right_lines[j]["raw"] + '\n', 'addition')
|
|
784
|
+
# Get line number AFTER inserting
|
|
785
|
+
actual_line = int(input_widget.index(tk.INSERT).split('.')[0]) - 1
|
|
786
|
+
if actual_line < 1:
|
|
787
|
+
actual_line = 1
|
|
788
|
+
self.diff_positions.append((actual_line, 'addition'))
|
|
789
|
+
self.diff_counts["additions"] += 1
|
|
790
|
+
|
|
791
|
+
elif tag == 'replace':
|
|
792
|
+
input_block = [l["raw"] for l in left_lines[i1:i2]]
|
|
793
|
+
output_block = [r["raw"] for r in right_lines[j1:j2]]
|
|
794
|
+
|
|
795
|
+
# Pad blocks to same length
|
|
796
|
+
while len(input_block) < len(output_block):
|
|
797
|
+
input_block.append("")
|
|
798
|
+
while len(output_block) < len(input_block):
|
|
799
|
+
output_block.append("")
|
|
800
|
+
|
|
801
|
+
for line1, line2 in zip(input_block, output_block):
|
|
802
|
+
if line1 and line2:
|
|
803
|
+
self._highlight_word_diffs(input_widget, [line1], output_widget, [line2])
|
|
804
|
+
# Get line number AFTER inserting
|
|
805
|
+
actual_line = int(input_widget.index(tk.INSERT).split('.')[0]) - 1
|
|
806
|
+
if actual_line < 1:
|
|
807
|
+
actual_line = 1
|
|
808
|
+
self.diff_positions.append((actual_line, 'modification'))
|
|
809
|
+
self.diff_counts["modifications"] += 1
|
|
810
|
+
elif line1:
|
|
811
|
+
input_widget.insert(tk.END, line1 + '\n', 'deletion')
|
|
812
|
+
output_widget.insert(tk.END, '\n')
|
|
813
|
+
actual_line = int(input_widget.index(tk.INSERT).split('.')[0]) - 1
|
|
814
|
+
if actual_line < 1:
|
|
815
|
+
actual_line = 1
|
|
816
|
+
self.diff_positions.append((actual_line, 'deletion'))
|
|
817
|
+
self.diff_counts["deletions"] += 1
|
|
818
|
+
elif line2:
|
|
819
|
+
input_widget.insert(tk.END, '\n')
|
|
820
|
+
output_widget.insert(tk.END, line2 + '\n', 'addition')
|
|
821
|
+
actual_line = int(input_widget.index(tk.INSERT).split('.')[0]) - 1
|
|
822
|
+
if actual_line < 1:
|
|
823
|
+
actual_line = 1
|
|
824
|
+
self.diff_positions.append((actual_line, 'addition'))
|
|
825
|
+
self.diff_counts["additions"] += 1
|
|
826
|
+
|
|
827
|
+
except Exception as e:
|
|
828
|
+
self.logger.error(f"Error in diff computation: {e}")
|
|
829
|
+
input_widget.insert(tk.END, input_text)
|
|
830
|
+
output_widget.insert(tk.END, output_text)
|
|
831
|
+
|
|
832
|
+
# Reset scroll position
|
|
833
|
+
input_widget.yview_moveto(0)
|
|
834
|
+
output_widget.yview_moveto(0)
|
|
835
|
+
self._setup_sync()
|
|
836
|
+
|
|
837
|
+
# Detect moved lines if enabled
|
|
838
|
+
if self.settings.get("detect_moved", False):
|
|
839
|
+
self._detect_moved_lines(input_widget, output_widget)
|
|
840
|
+
|
|
841
|
+
# Apply syntax highlighting if enabled
|
|
842
|
+
if self.settings.get("syntax_highlight", False):
|
|
843
|
+
self._apply_syntax_highlighting(input_widget)
|
|
844
|
+
self._apply_syntax_highlighting(output_widget)
|
|
845
|
+
|
|
846
|
+
# Update tab labels after comparison
|
|
847
|
+
self.update_tab_labels()
|
|
848
|
+
|
|
849
|
+
# Update diff summary bar
|
|
850
|
+
self._update_diff_summary()
|
|
851
|
+
|
|
852
|
+
def _detect_moved_lines(self, input_widget, output_widget):
|
|
853
|
+
"""
|
|
854
|
+
Detect lines that were moved (appear in both delete and insert sections).
|
|
855
|
+
Re-tags them as 'moved' instead of deletion/addition.
|
|
856
|
+
"""
|
|
857
|
+
try:
|
|
858
|
+
# Get all lines with deletion tag from input
|
|
859
|
+
deleted_lines = {}
|
|
860
|
+
for tag_range in input_widget.tag_ranges("deletion"):
|
|
861
|
+
if isinstance(tag_range, str):
|
|
862
|
+
continue
|
|
863
|
+
# Get line number from index
|
|
864
|
+
line_num = int(str(tag_range).split('.')[0])
|
|
865
|
+
line_content = input_widget.get(f"{line_num}.0", f"{line_num}.end").strip()
|
|
866
|
+
if line_content:
|
|
867
|
+
deleted_lines[line_content.lower()] = line_num
|
|
868
|
+
|
|
869
|
+
# Get all lines with addition tag from output
|
|
870
|
+
added_lines = {}
|
|
871
|
+
for tag_range in output_widget.tag_ranges("addition"):
|
|
872
|
+
if isinstance(tag_range, str):
|
|
873
|
+
continue
|
|
874
|
+
line_num = int(str(tag_range).split('.')[0])
|
|
875
|
+
line_content = output_widget.get(f"{line_num}.0", f"{line_num}.end").strip()
|
|
876
|
+
if line_content:
|
|
877
|
+
added_lines[line_content.lower()] = line_num
|
|
878
|
+
|
|
879
|
+
# Find lines that appear in both (moved lines)
|
|
880
|
+
moved_count = 0
|
|
881
|
+
for content, input_line in deleted_lines.items():
|
|
882
|
+
if content in added_lines:
|
|
883
|
+
output_line = added_lines[content]
|
|
884
|
+
# Re-tag as moved in both widgets
|
|
885
|
+
input_widget.tag_remove("deletion", f"{input_line}.0", f"{input_line}.end+1c")
|
|
886
|
+
input_widget.tag_add("moved", f"{input_line}.0", f"{input_line}.end+1c")
|
|
887
|
+
|
|
888
|
+
output_widget.tag_remove("addition", f"{output_line}.0", f"{output_line}.end+1c")
|
|
889
|
+
output_widget.tag_add("moved", f"{output_line}.0", f"{output_line}.end+1c")
|
|
890
|
+
|
|
891
|
+
moved_count += 1
|
|
892
|
+
|
|
893
|
+
# Update counts
|
|
894
|
+
if moved_count > 0:
|
|
895
|
+
self.diff_counts["moved"] = moved_count
|
|
896
|
+
# Reduce deletion/addition counts
|
|
897
|
+
self.diff_counts["deletions"] = max(0, self.diff_counts.get("deletions", 0) - moved_count)
|
|
898
|
+
self.diff_counts["additions"] = max(0, self.diff_counts.get("additions", 0) - moved_count)
|
|
899
|
+
|
|
900
|
+
except Exception as e:
|
|
901
|
+
self.logger.error(f"Error detecting moved lines: {e}")
|
|
902
|
+
|
|
903
|
+
def _apply_syntax_highlighting(self, widget):
|
|
904
|
+
"""
|
|
905
|
+
Apply syntax highlighting to code content in a text widget.
|
|
906
|
+
Uses regex patterns for common programming constructs.
|
|
907
|
+
"""
|
|
908
|
+
try:
|
|
909
|
+
content = widget.get("1.0", tk.END)
|
|
910
|
+
|
|
911
|
+
# Define patterns for syntax highlighting
|
|
912
|
+
patterns = [
|
|
913
|
+
# Python/JS keywords
|
|
914
|
+
(r'\b(def|class|import|from|return|if|elif|else|for|while|try|except|finally|with|as|'
|
|
915
|
+
r'raise|yield|lambda|and|or|not|in|is|True|False|None|async|await|'
|
|
916
|
+
r'function|const|let|var|new|this|typeof|instanceof|export|default)\b',
|
|
917
|
+
'syntax_keyword'),
|
|
918
|
+
|
|
919
|
+
# Triple-quoted strings (must come before single/double quotes)
|
|
920
|
+
(r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'', 'syntax_string'),
|
|
921
|
+
|
|
922
|
+
# Single and double quoted strings
|
|
923
|
+
(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', 'syntax_string'),
|
|
924
|
+
|
|
925
|
+
# Comments (Python # and JS //)
|
|
926
|
+
(r'#[^\n]*|//[^\n]*', 'syntax_comment'),
|
|
927
|
+
|
|
928
|
+
# Numbers (integers, floats, hex)
|
|
929
|
+
(r'\b(?:0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)\b', 'syntax_number'),
|
|
930
|
+
|
|
931
|
+
# Function definitions
|
|
932
|
+
(r'\b(?:def|function)\s+([a-zA-Z_][a-zA-Z0-9_]*)', 'syntax_function'),
|
|
933
|
+
|
|
934
|
+
# Decorators
|
|
935
|
+
(r'@[a-zA-Z_][a-zA-Z0-9_.]*', 'syntax_decorator'),
|
|
936
|
+
|
|
937
|
+
# Class definitions
|
|
938
|
+
(r'\bclass\s+([a-zA-Z_][a-zA-Z0-9_]*)', 'syntax_class'),
|
|
939
|
+
]
|
|
940
|
+
|
|
941
|
+
for pattern, tag in patterns:
|
|
942
|
+
for match in re.finditer(pattern, content):
|
|
943
|
+
start_idx = f"1.0+{match.start()}c"
|
|
944
|
+
end_idx = f"1.0+{match.end()}c"
|
|
945
|
+
|
|
946
|
+
# For function/class definitions, highlight just the name (group 1)
|
|
947
|
+
if tag in ('syntax_function', 'syntax_class') and match.lastindex:
|
|
948
|
+
name_start = match.start(1)
|
|
949
|
+
name_end = match.end(1)
|
|
950
|
+
start_idx = f"1.0+{name_start}c"
|
|
951
|
+
end_idx = f"1.0+{name_end}c"
|
|
952
|
+
|
|
953
|
+
widget.tag_add(tag, start_idx, end_idx)
|
|
954
|
+
|
|
955
|
+
# Ensure diff tags have higher priority (raise them above syntax tags)
|
|
956
|
+
for diff_tag in ['addition', 'deletion', 'modification', 'inline_add', 'inline_del', 'moved']:
|
|
957
|
+
widget.tag_raise(diff_tag)
|
|
958
|
+
|
|
959
|
+
except Exception as e:
|
|
960
|
+
self.logger.error(f"Error applying syntax highlighting: {e}")
|
|
961
|
+
|
|
962
|
+
def _highlight_word_diffs(self, w1, lines1, w2, lines2):
|
|
963
|
+
"""
|
|
964
|
+
Highlight word-level differences within a 'replace' block.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
w1: First text widget
|
|
968
|
+
lines1: Lines for first widget
|
|
969
|
+
w2: Second text widget
|
|
970
|
+
lines2: Lines for second widget
|
|
971
|
+
"""
|
|
972
|
+
# Check if character-level diff is enabled
|
|
973
|
+
use_char_diff = self.settings.get("char_level_diff", False)
|
|
974
|
+
|
|
975
|
+
for line1, line2 in zip(lines1, lines2):
|
|
976
|
+
w1.insert(tk.END, line1 + '\n', 'modification')
|
|
977
|
+
w2.insert(tk.END, line2 + '\n', 'modification')
|
|
978
|
+
|
|
979
|
+
line_start1 = w1.index(f"{w1.index(tk.INSERT)} -1 lines linestart")
|
|
980
|
+
line_start2 = w2.index(f"{w2.index(tk.INSERT)} -1 lines linestart")
|
|
981
|
+
|
|
982
|
+
if use_char_diff:
|
|
983
|
+
# Character-level diff
|
|
984
|
+
self._apply_char_diff(w1, line1, line_start1, w2, line2, line_start2)
|
|
985
|
+
else:
|
|
986
|
+
# Word-level diff
|
|
987
|
+
self._apply_word_diff(w1, line1, line_start1, w2, line2, line_start2)
|
|
988
|
+
|
|
989
|
+
def _apply_word_diff(self, w1, line1, line_start1, w2, line2, line_start2):
|
|
990
|
+
"""Apply word-level diff highlighting."""
|
|
991
|
+
try:
|
|
992
|
+
words1 = re.split(r'(\s+)', line1)
|
|
993
|
+
words2 = re.split(r'(\s+)', line2)
|
|
994
|
+
|
|
995
|
+
matcher = difflib.SequenceMatcher(None, words1, words2)
|
|
996
|
+
|
|
997
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
998
|
+
if tag == 'delete' or tag == 'replace':
|
|
999
|
+
start_char1 = len("".join(words1[:i1]))
|
|
1000
|
+
end_char1 = len("".join(words1[:i2]))
|
|
1001
|
+
w1.tag_add('inline_del', f"{line_start1}+{start_char1}c", f"{line_start1}+{end_char1}c")
|
|
1002
|
+
if tag == 'insert' or tag == 'replace':
|
|
1003
|
+
start_char2 = len("".join(words2[:j1]))
|
|
1004
|
+
end_char2 = len("".join(words2[:j2]))
|
|
1005
|
+
w2.tag_add('inline_add', f"{line_start2}+{start_char2}c", f"{line_start2}+{end_char2}c")
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
self.logger.error(f"Error in word-level diff highlighting: {e}")
|
|
1008
|
+
|
|
1009
|
+
def _apply_char_diff(self, w1, line1, line_start1, w2, line2, line_start2):
|
|
1010
|
+
"""Apply character-level diff highlighting."""
|
|
1011
|
+
try:
|
|
1012
|
+
# Split into individual characters
|
|
1013
|
+
chars1 = list(line1)
|
|
1014
|
+
chars2 = list(line2)
|
|
1015
|
+
|
|
1016
|
+
matcher = difflib.SequenceMatcher(None, chars1, chars2)
|
|
1017
|
+
|
|
1018
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
1019
|
+
if tag == 'delete' or tag == 'replace':
|
|
1020
|
+
w1.tag_add('inline_del', f"{line_start1}+{i1}c", f"{line_start1}+{i2}c")
|
|
1021
|
+
if tag == 'insert' or tag == 'replace':
|
|
1022
|
+
w2.tag_add('inline_add', f"{line_start2}+{j1}c", f"{line_start2}+{j2}c")
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
self.logger.error(f"Error in character-level diff highlighting: {e}")
|
|
1025
|
+
|
|
1026
|
+
def clear_all_input_tabs(self):
|
|
1027
|
+
"""Clear all input tabs."""
|
|
1028
|
+
for tab in self.input_tabs:
|
|
1029
|
+
tab.text.delete("1.0", tk.END)
|
|
1030
|
+
# Update tab labels after clearing
|
|
1031
|
+
self.update_tab_labels()
|
|
1032
|
+
|
|
1033
|
+
def clear_all_output_tabs(self):
|
|
1034
|
+
"""Clear all output tabs."""
|
|
1035
|
+
for tab in self.output_tabs:
|
|
1036
|
+
tab.text.delete("1.0", tk.END)
|
|
1037
|
+
# Update tab labels after clearing
|
|
1038
|
+
self.update_tab_labels()
|
|
1039
|
+
|
|
1040
|
+
def copy_to_clipboard(self):
|
|
1041
|
+
"""Copy active output tab content to clipboard."""
|
|
1042
|
+
try:
|
|
1043
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
1044
|
+
content = active_output_tab.text.get("1.0", tk.END)
|
|
1045
|
+
self.parent.clipboard_clear()
|
|
1046
|
+
self.parent.clipboard_append(content)
|
|
1047
|
+
except (tk.TclError, IndexError):
|
|
1048
|
+
pass
|
|
1049
|
+
|
|
1050
|
+
def copy_to_specific_input_tab(self, tab_index):
|
|
1051
|
+
"""
|
|
1052
|
+
Copy active output tab content to a specific input tab.
|
|
1053
|
+
|
|
1054
|
+
Args:
|
|
1055
|
+
tab_index: Index of the target input tab
|
|
1056
|
+
"""
|
|
1057
|
+
try:
|
|
1058
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
1059
|
+
content = active_output_tab.text.get("1.0", tk.END)
|
|
1060
|
+
|
|
1061
|
+
if 0 <= tab_index < len(self.input_tabs):
|
|
1062
|
+
self.input_tabs[tab_index].text.delete("1.0", tk.END)
|
|
1063
|
+
self.input_tabs[tab_index].text.insert("1.0", content)
|
|
1064
|
+
except (tk.TclError, IndexError):
|
|
1065
|
+
pass
|
|
1066
|
+
|
|
1067
|
+
def load_file_to_input(self):
|
|
1068
|
+
"""Load file content to the active input tab."""
|
|
1069
|
+
try:
|
|
1070
|
+
file_path = filedialog.askopenfilename(
|
|
1071
|
+
title="Select file to load",
|
|
1072
|
+
filetypes=[
|
|
1073
|
+
("Text files", "*.txt"),
|
|
1074
|
+
("All files", "*.*")
|
|
1075
|
+
],
|
|
1076
|
+
parent=self.parent
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
if file_path:
|
|
1080
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
1081
|
+
content = f.read()
|
|
1082
|
+
|
|
1083
|
+
# Load into active input tab
|
|
1084
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
1085
|
+
active_input_tab.text.delete("1.0", tk.END)
|
|
1086
|
+
active_input_tab.text.insert("1.0", content)
|
|
1087
|
+
|
|
1088
|
+
self.logger.info(f"Loaded file: {file_path}")
|
|
1089
|
+
except Exception as e:
|
|
1090
|
+
self.logger.error(f"Error loading file: {e}")
|
|
1091
|
+
self._show_error("Error", f"Could not load file: {str(e)}")
|
|
1092
|
+
|
|
1093
|
+
def update_tab_labels(self):
|
|
1094
|
+
"""Update tab labels based on content."""
|
|
1095
|
+
try:
|
|
1096
|
+
# Update input tab labels
|
|
1097
|
+
for i, tab in enumerate(self.input_tabs):
|
|
1098
|
+
content = tab.text.get("1.0", tk.END).strip()
|
|
1099
|
+
if content:
|
|
1100
|
+
# Get first few words for the label
|
|
1101
|
+
words = content.split()[:3]
|
|
1102
|
+
label = " ".join(words)
|
|
1103
|
+
if len(label) > 20:
|
|
1104
|
+
label = label[:17] + "..."
|
|
1105
|
+
if len(content.split()) > 3:
|
|
1106
|
+
label += "..."
|
|
1107
|
+
self.input_notebook.tab(i, text=f"{i+1}: {label}")
|
|
1108
|
+
else:
|
|
1109
|
+
self.input_notebook.tab(i, text=f"{i+1}:")
|
|
1110
|
+
|
|
1111
|
+
# Update output tab labels
|
|
1112
|
+
for i, tab in enumerate(self.output_tabs):
|
|
1113
|
+
content = tab.text.get("1.0", tk.END).strip()
|
|
1114
|
+
if content:
|
|
1115
|
+
# Get first few words for the label
|
|
1116
|
+
words = content.split()[:3]
|
|
1117
|
+
label = " ".join(words)
|
|
1118
|
+
if len(label) > 20:
|
|
1119
|
+
label = label[:17] + "..."
|
|
1120
|
+
if len(content.split()) > 3:
|
|
1121
|
+
label += "..."
|
|
1122
|
+
self.output_notebook.tab(i, text=f"{i+1}: {label}")
|
|
1123
|
+
else:
|
|
1124
|
+
self.output_notebook.tab(i, text=f"{i+1}:")
|
|
1125
|
+
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
self.logger.error(f"Error updating tab labels: {e}")
|
|
1128
|
+
|
|
1129
|
+
def _update_diff_summary(self):
|
|
1130
|
+
"""Update the diff summary bar with current comparison results."""
|
|
1131
|
+
if not self.diff_summary_bar:
|
|
1132
|
+
return
|
|
1133
|
+
|
|
1134
|
+
total_diffs = len(self.diff_positions)
|
|
1135
|
+
adds = self.diff_counts.get("additions", 0)
|
|
1136
|
+
dels = self.diff_counts.get("deletions", 0)
|
|
1137
|
+
mods = self.diff_counts.get("modifications", 0)
|
|
1138
|
+
moved = self.diff_counts.get("moved", 0)
|
|
1139
|
+
|
|
1140
|
+
if total_diffs == 0 and moved == 0:
|
|
1141
|
+
summary_text = "No differences found | 100% similar"
|
|
1142
|
+
self.prev_diff_btn.state(['disabled'])
|
|
1143
|
+
self.next_diff_btn.state(['disabled'])
|
|
1144
|
+
else:
|
|
1145
|
+
parts = [f"+{adds} additions", f"-{dels} deletions", f"~{mods} modifications"]
|
|
1146
|
+
if moved > 0:
|
|
1147
|
+
parts.append(f"↔{moved} moved")
|
|
1148
|
+
parts.append(f"{self.similarity_score:.1f}% similar")
|
|
1149
|
+
summary_text = " | ".join(parts)
|
|
1150
|
+
self.prev_diff_btn.state(['!disabled'])
|
|
1151
|
+
self.next_diff_btn.state(['!disabled'])
|
|
1152
|
+
|
|
1153
|
+
self.diff_summary_bar.config(text=summary_text)
|
|
1154
|
+
|
|
1155
|
+
def _goto_prev_diff(self):
|
|
1156
|
+
"""Navigate to the previous difference."""
|
|
1157
|
+
if not self.diff_positions:
|
|
1158
|
+
return
|
|
1159
|
+
|
|
1160
|
+
if self.current_diff_index <= 0:
|
|
1161
|
+
self.current_diff_index = len(self.diff_positions) - 1
|
|
1162
|
+
else:
|
|
1163
|
+
self.current_diff_index -= 1
|
|
1164
|
+
|
|
1165
|
+
self._scroll_to_diff(self.current_diff_index)
|
|
1166
|
+
|
|
1167
|
+
def _goto_next_diff(self):
|
|
1168
|
+
"""Navigate to the next difference."""
|
|
1169
|
+
if not self.diff_positions:
|
|
1170
|
+
return
|
|
1171
|
+
|
|
1172
|
+
if self.current_diff_index >= len(self.diff_positions) - 1:
|
|
1173
|
+
self.current_diff_index = 0
|
|
1174
|
+
else:
|
|
1175
|
+
self.current_diff_index += 1
|
|
1176
|
+
|
|
1177
|
+
self._scroll_to_diff(self.current_diff_index)
|
|
1178
|
+
|
|
1179
|
+
def _scroll_to_diff(self, diff_index):
|
|
1180
|
+
"""Scroll both text widgets to show the specified difference."""
|
|
1181
|
+
try:
|
|
1182
|
+
if diff_index < 0 or diff_index >= len(self.diff_positions):
|
|
1183
|
+
return
|
|
1184
|
+
|
|
1185
|
+
line_num, diff_type = self.diff_positions[diff_index]
|
|
1186
|
+
|
|
1187
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
1188
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
1189
|
+
|
|
1190
|
+
input_widget = active_input_tab.text
|
|
1191
|
+
output_widget = active_output_tab.text
|
|
1192
|
+
|
|
1193
|
+
# Line index for the diff
|
|
1194
|
+
line_index = f"{line_num}.0"
|
|
1195
|
+
line_end = f"{line_num}.end"
|
|
1196
|
+
|
|
1197
|
+
# Remove any previous navigation highlight
|
|
1198
|
+
input_widget.tag_remove("nav_highlight", "1.0", tk.END)
|
|
1199
|
+
output_widget.tag_remove("nav_highlight", "1.0", tk.END)
|
|
1200
|
+
|
|
1201
|
+
# Configure navigation highlight tag (bright yellow background with high priority)
|
|
1202
|
+
input_widget.tag_configure("nav_highlight", background="#ffff00", foreground="#000000")
|
|
1203
|
+
output_widget.tag_configure("nav_highlight", background="#ffff00", foreground="#000000")
|
|
1204
|
+
|
|
1205
|
+
# Add highlight to current diff line
|
|
1206
|
+
input_widget.tag_add("nav_highlight", line_index, line_end)
|
|
1207
|
+
output_widget.tag_add("nav_highlight", line_index, line_end)
|
|
1208
|
+
|
|
1209
|
+
# Raise nav_highlight above all other tags so it's visible
|
|
1210
|
+
input_widget.tag_raise("nav_highlight")
|
|
1211
|
+
output_widget.tag_raise("nav_highlight")
|
|
1212
|
+
|
|
1213
|
+
# Move cursor to the beginning of the line
|
|
1214
|
+
input_widget.mark_set(tk.INSERT, line_index)
|
|
1215
|
+
output_widget.mark_set(tk.INSERT, line_index)
|
|
1216
|
+
|
|
1217
|
+
# Focus the input widget
|
|
1218
|
+
input_widget.focus_set()
|
|
1219
|
+
|
|
1220
|
+
# Scroll to make the line visible (centered if possible)
|
|
1221
|
+
input_widget.see(line_index)
|
|
1222
|
+
output_widget.see(line_index)
|
|
1223
|
+
|
|
1224
|
+
# Force update of line numbers if available
|
|
1225
|
+
if hasattr(active_input_tab, '_on_text_modified'):
|
|
1226
|
+
active_input_tab._on_text_modified()
|
|
1227
|
+
if hasattr(active_output_tab, '_on_text_modified'):
|
|
1228
|
+
active_output_tab._on_text_modified()
|
|
1229
|
+
|
|
1230
|
+
# Update summary to show current position
|
|
1231
|
+
total = len(self.diff_positions)
|
|
1232
|
+
current = diff_index + 1
|
|
1233
|
+
base_summary = self.diff_summary_bar.cget("text").split(" | Diff ")[0]
|
|
1234
|
+
self.diff_summary_bar.config(text=f"{base_summary} | Diff {current}/{total}")
|
|
1235
|
+
|
|
1236
|
+
except (tk.TclError, IndexError) as e:
|
|
1237
|
+
self.logger.error(f"Error scrolling to diff: {e}")
|
|
1238
|
+
|
|
1239
|
+
def _export_to_html(self):
|
|
1240
|
+
"""Export the current diff comparison to an HTML file."""
|
|
1241
|
+
try:
|
|
1242
|
+
active_input_idx = self.input_notebook.index("current")
|
|
1243
|
+
active_output_idx = self.output_notebook.index("current")
|
|
1244
|
+
|
|
1245
|
+
input_text = self.input_tabs[active_input_idx].text.get("1.0", tk.END)
|
|
1246
|
+
output_text = self.output_tabs[active_output_idx].text.get("1.0", tk.END)
|
|
1247
|
+
|
|
1248
|
+
# Remove trailing newlines
|
|
1249
|
+
if input_text.endswith('\n'):
|
|
1250
|
+
input_text = input_text[:-1]
|
|
1251
|
+
if output_text.endswith('\n'):
|
|
1252
|
+
output_text = output_text[:-1]
|
|
1253
|
+
|
|
1254
|
+
if not input_text.strip() and not output_text.strip():
|
|
1255
|
+
messagebox.showinfo("Export", "No content to export.", parent=self.parent)
|
|
1256
|
+
return
|
|
1257
|
+
|
|
1258
|
+
# Ask for save location
|
|
1259
|
+
file_path = filedialog.asksaveasfilename(
|
|
1260
|
+
title="Export Diff as HTML",
|
|
1261
|
+
defaultextension=".html",
|
|
1262
|
+
filetypes=[("HTML files", "*.html"), ("All files", "*.*")],
|
|
1263
|
+
parent=self.parent
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
if not file_path:
|
|
1267
|
+
return
|
|
1268
|
+
|
|
1269
|
+
# Generate HTML diff
|
|
1270
|
+
html_diff = difflib.HtmlDiff(wrapcolumn=80)
|
|
1271
|
+
html_content = html_diff.make_file(
|
|
1272
|
+
input_text.splitlines(),
|
|
1273
|
+
output_text.splitlines(),
|
|
1274
|
+
fromdesc="Input",
|
|
1275
|
+
todesc="Output",
|
|
1276
|
+
context=False
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
# Add custom styling for better appearance
|
|
1280
|
+
custom_css = """
|
|
1281
|
+
<style>
|
|
1282
|
+
body { font-family: 'Segoe UI', Tahoma, sans-serif; margin: 20px; }
|
|
1283
|
+
table.diff { border-collapse: collapse; width: 100%; }
|
|
1284
|
+
.diff_header { background-color: #f0f0f0; }
|
|
1285
|
+
.diff_next { background-color: #e0e0e0; }
|
|
1286
|
+
td { padding: 2px 8px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
|
1287
|
+
.diff_add { background-color: #e6ffed; }
|
|
1288
|
+
.diff_chg { background-color: #e6f7ff; }
|
|
1289
|
+
.diff_sub { background-color: #ffebe9; }
|
|
1290
|
+
.summary { margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 5px; }
|
|
1291
|
+
</style>
|
|
1292
|
+
"""
|
|
1293
|
+
|
|
1294
|
+
# Insert summary and custom CSS
|
|
1295
|
+
summary_html = f"""
|
|
1296
|
+
<div class="summary">
|
|
1297
|
+
<strong>Diff Summary:</strong>
|
|
1298
|
+
+{self.diff_counts.get('additions', 0)} additions |
|
|
1299
|
+
-{self.diff_counts.get('deletions', 0)} deletions |
|
|
1300
|
+
~{self.diff_counts.get('modifications', 0)} modifications |
|
|
1301
|
+
{self.similarity_score:.1f}% similar
|
|
1302
|
+
</div>
|
|
1303
|
+
"""
|
|
1304
|
+
|
|
1305
|
+
html_content = html_content.replace("</head>", f"{custom_css}</head>")
|
|
1306
|
+
html_content = html_content.replace("<body>", f"<body>{summary_html}")
|
|
1307
|
+
|
|
1308
|
+
# Write to file
|
|
1309
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
1310
|
+
f.write(html_content)
|
|
1311
|
+
|
|
1312
|
+
self.logger.info(f"Exported diff to: {file_path}")
|
|
1313
|
+
messagebox.showinfo("Export Complete", f"Diff exported to:\n{file_path}", parent=self.parent)
|
|
1314
|
+
|
|
1315
|
+
except Exception as e:
|
|
1316
|
+
self.logger.error(f"Error exporting to HTML: {e}")
|
|
1317
|
+
self._show_error("Export Error", f"Could not export diff: {str(e)}")
|
|
1318
|
+
|
|
1319
|
+
def _on_input_filter_changed(self, *args):
|
|
1320
|
+
"""Handle input filter text changes."""
|
|
1321
|
+
self._apply_input_filter()
|
|
1322
|
+
|
|
1323
|
+
def _on_output_filter_changed(self, *args):
|
|
1324
|
+
"""Handle output filter text changes."""
|
|
1325
|
+
self._apply_output_filter()
|
|
1326
|
+
|
|
1327
|
+
def _clear_input_filter(self):
|
|
1328
|
+
"""Clear the input filter."""
|
|
1329
|
+
self.input_filter_var.set("")
|
|
1330
|
+
|
|
1331
|
+
def _clear_output_filter(self):
|
|
1332
|
+
"""Clear the output filter."""
|
|
1333
|
+
self.output_filter_var.set("")
|
|
1334
|
+
|
|
1335
|
+
def _apply_input_filter(self):
|
|
1336
|
+
"""Apply line filter to the active input tab."""
|
|
1337
|
+
try:
|
|
1338
|
+
active_idx = self.input_notebook.index("current")
|
|
1339
|
+
current_tab = self.input_tabs[active_idx]
|
|
1340
|
+
filter_text = self.input_filter_var.get().strip()
|
|
1341
|
+
|
|
1342
|
+
# Store original content if not already stored
|
|
1343
|
+
if active_idx not in self.input_original_content:
|
|
1344
|
+
self.input_original_content[active_idx] = current_tab.text.get("1.0", tk.END)
|
|
1345
|
+
|
|
1346
|
+
original_content = self.input_original_content[active_idx]
|
|
1347
|
+
|
|
1348
|
+
if filter_text:
|
|
1349
|
+
# Apply filter
|
|
1350
|
+
lines = original_content.split('\n')
|
|
1351
|
+
|
|
1352
|
+
if self.input_regex_mode.get():
|
|
1353
|
+
# Regex mode
|
|
1354
|
+
try:
|
|
1355
|
+
pattern = re.compile(filter_text, re.IGNORECASE)
|
|
1356
|
+
filtered_lines = [line for line in lines if pattern.search(line)]
|
|
1357
|
+
except re.error as e:
|
|
1358
|
+
self.logger.warning(f"Invalid regex pattern: {e}")
|
|
1359
|
+
# Fallback to literal search on regex error
|
|
1360
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
1361
|
+
else:
|
|
1362
|
+
# Simple substring match
|
|
1363
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
1364
|
+
|
|
1365
|
+
filtered_content = '\n'.join(filtered_lines)
|
|
1366
|
+
|
|
1367
|
+
current_tab.text.delete("1.0", tk.END)
|
|
1368
|
+
current_tab.text.insert("1.0", filtered_content)
|
|
1369
|
+
else:
|
|
1370
|
+
# Restore original content
|
|
1371
|
+
current_tab.text.delete("1.0", tk.END)
|
|
1372
|
+
current_tab.text.insert("1.0", original_content)
|
|
1373
|
+
# Clear stored content
|
|
1374
|
+
if active_idx in self.input_original_content:
|
|
1375
|
+
del self.input_original_content[active_idx]
|
|
1376
|
+
|
|
1377
|
+
# Update statistics
|
|
1378
|
+
self.update_statistics()
|
|
1379
|
+
|
|
1380
|
+
except Exception as e:
|
|
1381
|
+
self.logger.error(f"Error applying input filter: {e}")
|
|
1382
|
+
|
|
1383
|
+
def _apply_output_filter(self):
|
|
1384
|
+
"""Apply line filter to the active output tab."""
|
|
1385
|
+
try:
|
|
1386
|
+
active_idx = self.output_notebook.index("current")
|
|
1387
|
+
current_tab = self.output_tabs[active_idx]
|
|
1388
|
+
filter_text = self.output_filter_var.get().strip()
|
|
1389
|
+
|
|
1390
|
+
# Store original content if not already stored
|
|
1391
|
+
if active_idx not in self.output_original_content:
|
|
1392
|
+
self.output_original_content[active_idx] = current_tab.text.get("1.0", tk.END)
|
|
1393
|
+
|
|
1394
|
+
original_content = self.output_original_content[active_idx]
|
|
1395
|
+
|
|
1396
|
+
if filter_text:
|
|
1397
|
+
# Apply filter
|
|
1398
|
+
lines = original_content.split('\n')
|
|
1399
|
+
|
|
1400
|
+
if self.output_regex_mode.get():
|
|
1401
|
+
# Regex mode
|
|
1402
|
+
try:
|
|
1403
|
+
pattern = re.compile(filter_text, re.IGNORECASE)
|
|
1404
|
+
filtered_lines = [line for line in lines if pattern.search(line)]
|
|
1405
|
+
except re.error as e:
|
|
1406
|
+
self.logger.warning(f"Invalid regex pattern: {e}")
|
|
1407
|
+
# Fallback to literal search on regex error
|
|
1408
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
1409
|
+
else:
|
|
1410
|
+
# Simple substring match
|
|
1411
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
1412
|
+
|
|
1413
|
+
filtered_content = '\n'.join(filtered_lines)
|
|
1414
|
+
|
|
1415
|
+
current_tab.text.delete("1.0", tk.END)
|
|
1416
|
+
current_tab.text.insert("1.0", filtered_content)
|
|
1417
|
+
else:
|
|
1418
|
+
# Restore original content
|
|
1419
|
+
current_tab.text.delete("1.0", tk.END)
|
|
1420
|
+
current_tab.text.insert("1.0", original_content)
|
|
1421
|
+
# Clear stored content
|
|
1422
|
+
if active_idx in self.output_original_content:
|
|
1423
|
+
del self.output_original_content[active_idx]
|
|
1424
|
+
|
|
1425
|
+
# Update statistics
|
|
1426
|
+
self.update_statistics()
|
|
1427
|
+
|
|
1428
|
+
except Exception as e:
|
|
1429
|
+
self.logger.error(f"Error applying output filter: {e}")
|
|
1430
|
+
|
|
1431
|
+
def get_settings(self):
|
|
1432
|
+
"""Get current diff viewer settings."""
|
|
1433
|
+
return self.settings.copy()
|
|
1434
|
+
|
|
1435
|
+
def update_settings(self, settings):
|
|
1436
|
+
"""
|
|
1437
|
+
Update diff viewer settings.
|
|
1438
|
+
|
|
1439
|
+
Args:
|
|
1440
|
+
settings: Dictionary of settings to update
|
|
1441
|
+
"""
|
|
1442
|
+
self.settings.update(settings)
|
|
1443
|
+
|
|
1444
|
+
def apply_font_to_widgets(self, font_tuple):
|
|
1445
|
+
"""
|
|
1446
|
+
Apply font to all text widgets in the diff viewer.
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
font_tuple: Tuple of (font_family, font_size)
|
|
1450
|
+
"""
|
|
1451
|
+
try:
|
|
1452
|
+
for tab in self.input_tabs:
|
|
1453
|
+
if hasattr(tab, 'text'):
|
|
1454
|
+
tab.text.configure(font=font_tuple)
|
|
1455
|
+
|
|
1456
|
+
for tab in self.output_tabs:
|
|
1457
|
+
if hasattr(tab, 'text'):
|
|
1458
|
+
tab.text.configure(font=font_tuple)
|
|
1459
|
+
|
|
1460
|
+
self.logger.debug(f"Applied font {font_tuple} to diff viewer text widgets")
|
|
1461
|
+
except Exception as e:
|
|
1462
|
+
self.logger.error(f"Error applying font to diff viewer: {e}")
|
|
1463
|
+
|
|
1464
|
+
def update_statistics(self):
|
|
1465
|
+
"""Update statistics bars for the active tabs."""
|
|
1466
|
+
try:
|
|
1467
|
+
# Get active tab indices
|
|
1468
|
+
active_input_idx = self.input_notebook.index("current")
|
|
1469
|
+
active_output_idx = self.output_notebook.index("current")
|
|
1470
|
+
|
|
1471
|
+
# Get text from active tabs
|
|
1472
|
+
input_text = self.input_tabs[active_input_idx].text.get("1.0", tk.END)
|
|
1473
|
+
output_text = self.output_tabs[active_output_idx].text.get("1.0", tk.END)
|
|
1474
|
+
|
|
1475
|
+
# Update input statistics
|
|
1476
|
+
if self.input_stats_bar:
|
|
1477
|
+
self._update_stats_bar(self.input_stats_bar, input_text)
|
|
1478
|
+
|
|
1479
|
+
# Update output statistics
|
|
1480
|
+
if self.output_stats_bar:
|
|
1481
|
+
self._update_stats_bar(self.output_stats_bar, output_text)
|
|
1482
|
+
|
|
1483
|
+
except Exception as e:
|
|
1484
|
+
self.logger.error(f"Error updating statistics: {e}")
|
|
1485
|
+
|
|
1486
|
+
def _update_stats_bar(self, stats_bar, text):
|
|
1487
|
+
"""
|
|
1488
|
+
Update a statistics bar with text statistics.
|
|
1489
|
+
|
|
1490
|
+
Args:
|
|
1491
|
+
stats_bar: The label widget to update
|
|
1492
|
+
text: The text to analyze
|
|
1493
|
+
"""
|
|
1494
|
+
try:
|
|
1495
|
+
# Remove trailing newline that tkinter adds
|
|
1496
|
+
if text.endswith('\n'):
|
|
1497
|
+
text = text[:-1]
|
|
1498
|
+
|
|
1499
|
+
# Handle empty text
|
|
1500
|
+
if not text:
|
|
1501
|
+
stats_bar.config(text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0")
|
|
1502
|
+
return
|
|
1503
|
+
|
|
1504
|
+
stripped_text = text.strip()
|
|
1505
|
+
char_count = len(stripped_text)
|
|
1506
|
+
byte_count = len(text.encode('utf-8'))
|
|
1507
|
+
|
|
1508
|
+
# Count lines (more accurate)
|
|
1509
|
+
line_count = text.count('\n') + 1
|
|
1510
|
+
|
|
1511
|
+
# Count words
|
|
1512
|
+
if char_count == 0:
|
|
1513
|
+
word_count = 0
|
|
1514
|
+
else:
|
|
1515
|
+
words = [word for word in stripped_text.split() if word]
|
|
1516
|
+
word_count = len(words)
|
|
1517
|
+
|
|
1518
|
+
# Count sentences using regex pattern that handles abbreviations better
|
|
1519
|
+
# Looks for sentence-ending punctuation followed by space or end of string
|
|
1520
|
+
sentence_pattern = r'[.!?]+(?:\s|$)'
|
|
1521
|
+
sentence_matches = re.findall(sentence_pattern, text)
|
|
1522
|
+
sentence_count = len(sentence_matches)
|
|
1523
|
+
if sentence_count == 0 and char_count > 0:
|
|
1524
|
+
sentence_count = 1
|
|
1525
|
+
|
|
1526
|
+
# Token estimation
|
|
1527
|
+
token_count = max(1, round(char_count / 4)) if char_count > 0 else 0
|
|
1528
|
+
|
|
1529
|
+
# Format bytes
|
|
1530
|
+
if byte_count < 1024:
|
|
1531
|
+
formatted_bytes = f"{byte_count}"
|
|
1532
|
+
elif byte_count < 1024 * 1024:
|
|
1533
|
+
formatted_bytes = f"{byte_count / 1024:.1f}K"
|
|
1534
|
+
else:
|
|
1535
|
+
formatted_bytes = f"{byte_count / (1024 * 1024):.1f}M"
|
|
1536
|
+
|
|
1537
|
+
stats_bar.config(
|
|
1538
|
+
text=f"Bytes: {formatted_bytes} | Word: {word_count} | Sentence: {sentence_count} | Line: {line_count} | Tokens: {token_count}"
|
|
1539
|
+
)
|
|
1540
|
+
except Exception as e:
|
|
1541
|
+
self.logger.error(f"Error calculating statistics: {e}")
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
class DiffViewerSettingsWidget:
|
|
1545
|
+
"""Settings widget for the diff viewer tool."""
|
|
1546
|
+
|
|
1547
|
+
def __init__(self, parent, diff_viewer, on_setting_change=None):
|
|
1548
|
+
"""
|
|
1549
|
+
Initialize the settings widget.
|
|
1550
|
+
|
|
1551
|
+
Args:
|
|
1552
|
+
parent: Parent tkinter widget
|
|
1553
|
+
diff_viewer: DiffViewerWidget instance
|
|
1554
|
+
on_setting_change: Callback function for setting changes
|
|
1555
|
+
"""
|
|
1556
|
+
self.parent = parent
|
|
1557
|
+
self.diff_viewer = diff_viewer
|
|
1558
|
+
self.on_setting_change = on_setting_change
|
|
1559
|
+
|
|
1560
|
+
# Get current settings
|
|
1561
|
+
settings = diff_viewer.get_settings()
|
|
1562
|
+
default_option = settings.get("option", "ignore_case")
|
|
1563
|
+
default_char_level = settings.get("char_level_diff", False)
|
|
1564
|
+
default_detect_moved = settings.get("detect_moved", False)
|
|
1565
|
+
default_syntax = settings.get("syntax_highlight", False)
|
|
1566
|
+
|
|
1567
|
+
# Create option variables
|
|
1568
|
+
self.option_var = tk.StringVar(value=default_option)
|
|
1569
|
+
self.char_level_var = tk.BooleanVar(value=default_char_level)
|
|
1570
|
+
self.detect_moved_var = tk.BooleanVar(value=default_detect_moved)
|
|
1571
|
+
self.syntax_var = tk.BooleanVar(value=default_syntax)
|
|
1572
|
+
|
|
1573
|
+
# Create UI
|
|
1574
|
+
self._create_ui()
|
|
1575
|
+
|
|
1576
|
+
def _create_ui(self):
|
|
1577
|
+
"""Create the settings UI in two rows for better layout."""
|
|
1578
|
+
# Row 1: Comparison mode radio buttons
|
|
1579
|
+
row1 = ttk.Frame(self.parent)
|
|
1580
|
+
row1.pack(fill=tk.X, pady=(0, 3))
|
|
1581
|
+
|
|
1582
|
+
ttk.Label(row1, text="Mode:").pack(side=tk.LEFT, padx=(0, 5))
|
|
1583
|
+
|
|
1584
|
+
ttk.Radiobutton(
|
|
1585
|
+
row1,
|
|
1586
|
+
text="Ignore case",
|
|
1587
|
+
variable=self.option_var,
|
|
1588
|
+
value="ignore_case",
|
|
1589
|
+
command=self._on_option_change
|
|
1590
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1591
|
+
|
|
1592
|
+
ttk.Radiobutton(
|
|
1593
|
+
row1,
|
|
1594
|
+
text="Match case",
|
|
1595
|
+
variable=self.option_var,
|
|
1596
|
+
value="match_case",
|
|
1597
|
+
command=self._on_option_change
|
|
1598
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1599
|
+
|
|
1600
|
+
ttk.Radiobutton(
|
|
1601
|
+
row1,
|
|
1602
|
+
text="Ignore whitespace",
|
|
1603
|
+
variable=self.option_var,
|
|
1604
|
+
value="ignore_whitespace",
|
|
1605
|
+
command=self._on_option_change
|
|
1606
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1607
|
+
|
|
1608
|
+
ttk.Radiobutton(
|
|
1609
|
+
row1,
|
|
1610
|
+
text="Ignore punctuation",
|
|
1611
|
+
variable=self.option_var,
|
|
1612
|
+
value="ignore_punctuation",
|
|
1613
|
+
command=self._on_option_change
|
|
1614
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1615
|
+
|
|
1616
|
+
ttk.Radiobutton(
|
|
1617
|
+
row1,
|
|
1618
|
+
text="Sentences",
|
|
1619
|
+
variable=self.option_var,
|
|
1620
|
+
value="sentence_level",
|
|
1621
|
+
command=self._on_option_change
|
|
1622
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1623
|
+
|
|
1624
|
+
# Row 2: Action buttons and options
|
|
1625
|
+
row2 = ttk.Frame(self.parent)
|
|
1626
|
+
row2.pack(fill=tk.X)
|
|
1627
|
+
|
|
1628
|
+
ttk.Button(
|
|
1629
|
+
row2,
|
|
1630
|
+
text="Compare Active Tabs",
|
|
1631
|
+
command=self._run_comparison
|
|
1632
|
+
).pack(side=tk.LEFT, padx=(0, 10))
|
|
1633
|
+
|
|
1634
|
+
ttk.Label(row2, text="|").pack(side=tk.LEFT, padx=5)
|
|
1635
|
+
|
|
1636
|
+
ttk.Label(row2, text="Options:").pack(side=tk.LEFT, padx=(0, 5))
|
|
1637
|
+
|
|
1638
|
+
# Character-level diff checkbox
|
|
1639
|
+
ttk.Checkbutton(
|
|
1640
|
+
row2,
|
|
1641
|
+
text="Char diff",
|
|
1642
|
+
variable=self.char_level_var,
|
|
1643
|
+
command=self._on_char_level_change
|
|
1644
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1645
|
+
|
|
1646
|
+
# Detect moved lines checkbox
|
|
1647
|
+
ttk.Checkbutton(
|
|
1648
|
+
row2,
|
|
1649
|
+
text="Detect moved",
|
|
1650
|
+
variable=self.detect_moved_var,
|
|
1651
|
+
command=self._on_detect_moved_change
|
|
1652
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1653
|
+
|
|
1654
|
+
# Syntax highlighting checkbox
|
|
1655
|
+
ttk.Checkbutton(
|
|
1656
|
+
row2,
|
|
1657
|
+
text="Syntax",
|
|
1658
|
+
variable=self.syntax_var,
|
|
1659
|
+
command=self._on_syntax_change
|
|
1660
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1661
|
+
|
|
1662
|
+
ttk.Label(row2, text="|").pack(side=tk.LEFT, padx=5)
|
|
1663
|
+
|
|
1664
|
+
ttk.Button(
|
|
1665
|
+
row2,
|
|
1666
|
+
text="List Comparator",
|
|
1667
|
+
command=self._launch_list_comparator
|
|
1668
|
+
).pack(side=tk.LEFT, padx=5)
|
|
1669
|
+
|
|
1670
|
+
def _on_option_change(self):
|
|
1671
|
+
"""Handle option change."""
|
|
1672
|
+
option = self.option_var.get()
|
|
1673
|
+
|
|
1674
|
+
# Show confirmation dialog for sentence mode
|
|
1675
|
+
if option == "sentence_level":
|
|
1676
|
+
if not self._confirm_sentence_mode():
|
|
1677
|
+
# User cancelled - revert to previous option
|
|
1678
|
+
previous_option = self.diff_viewer.settings.get("option", "ignore_case")
|
|
1679
|
+
self.option_var.set(previous_option)
|
|
1680
|
+
return
|
|
1681
|
+
|
|
1682
|
+
self.diff_viewer.update_settings({"option": option})
|
|
1683
|
+
|
|
1684
|
+
if self.on_setting_change:
|
|
1685
|
+
self.on_setting_change("Diff Viewer", {"option": option})
|
|
1686
|
+
|
|
1687
|
+
def _confirm_sentence_mode(self):
|
|
1688
|
+
"""Show confirmation dialog for sentence mode."""
|
|
1689
|
+
message = (
|
|
1690
|
+
"Sentence Mode restructures text for comparison.\n\n"
|
|
1691
|
+
"• Text will be split into sentences (not lines)\n"
|
|
1692
|
+
"• Each sentence appears on its own line\n"
|
|
1693
|
+
"• Original line breaks will not be preserved\n\n"
|
|
1694
|
+
"This is useful for comparing prose where sentences\n"
|
|
1695
|
+
"may span multiple lines or be wrapped differently.\n\n"
|
|
1696
|
+
"Continue with Sentence Mode?"
|
|
1697
|
+
)
|
|
1698
|
+
return messagebox.askyesno(
|
|
1699
|
+
"Sentence Mode",
|
|
1700
|
+
message,
|
|
1701
|
+
icon=messagebox.WARNING
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
def _on_char_level_change(self):
|
|
1705
|
+
"""Handle character-level diff toggle."""
|
|
1706
|
+
char_level = self.char_level_var.get()
|
|
1707
|
+
self.diff_viewer.update_settings({"char_level_diff": char_level})
|
|
1708
|
+
|
|
1709
|
+
# Re-run comparison with new setting
|
|
1710
|
+
self._run_comparison()
|
|
1711
|
+
|
|
1712
|
+
if self.on_setting_change:
|
|
1713
|
+
self.on_setting_change("Diff Viewer", {"char_level_diff": char_level})
|
|
1714
|
+
|
|
1715
|
+
def _on_detect_moved_change(self):
|
|
1716
|
+
"""Handle detect moved lines toggle."""
|
|
1717
|
+
detect_moved = self.detect_moved_var.get()
|
|
1718
|
+
self.diff_viewer.update_settings({"detect_moved": detect_moved})
|
|
1719
|
+
|
|
1720
|
+
# Re-run comparison with new setting
|
|
1721
|
+
self._run_comparison()
|
|
1722
|
+
|
|
1723
|
+
if self.on_setting_change:
|
|
1724
|
+
self.on_setting_change("Diff Viewer", {"detect_moved": detect_moved})
|
|
1725
|
+
|
|
1726
|
+
def _on_syntax_change(self):
|
|
1727
|
+
"""Handle syntax highlighting toggle."""
|
|
1728
|
+
syntax = self.syntax_var.get()
|
|
1729
|
+
self.diff_viewer.update_settings({"syntax_highlight": syntax})
|
|
1730
|
+
|
|
1731
|
+
# Re-run comparison with new setting
|
|
1732
|
+
self._run_comparison()
|
|
1733
|
+
|
|
1734
|
+
if self.on_setting_change:
|
|
1735
|
+
self.on_setting_change("Diff Viewer", {"syntax_highlight": syntax})
|
|
1736
|
+
|
|
1737
|
+
def _run_comparison(self):
|
|
1738
|
+
"""Run the diff comparison."""
|
|
1739
|
+
option = self.option_var.get()
|
|
1740
|
+
# Reset comparison source and clean widget content of accumulated blanks
|
|
1741
|
+
self.diff_viewer.reset_comparison_source()
|
|
1742
|
+
self.diff_viewer.run_comparison(option)
|
|
1743
|
+
|
|
1744
|
+
def _launch_list_comparator(self):
|
|
1745
|
+
"""Launch the list comparator application."""
|
|
1746
|
+
try:
|
|
1747
|
+
# Try to use the parent app's integrated list comparator if available
|
|
1748
|
+
# Check on the diff_viewer instance (not self, which is the settings widget)
|
|
1749
|
+
if hasattr(self.diff_viewer, 'open_list_comparator') and callable(self.diff_viewer.open_list_comparator):
|
|
1750
|
+
self.diff_viewer.logger.info("✅ Found open_list_comparator method, calling it...")
|
|
1751
|
+
self.diff_viewer.open_list_comparator()
|
|
1752
|
+
self.diff_viewer.logger.info("✅ List Comparator launched via parent app")
|
|
1753
|
+
else:
|
|
1754
|
+
# Fallback: Launch as subprocess (standalone mode)
|
|
1755
|
+
import subprocess
|
|
1756
|
+
import os
|
|
1757
|
+
import sys
|
|
1758
|
+
|
|
1759
|
+
# Get the directory where the current script is located
|
|
1760
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1761
|
+
list_comparator_path = os.path.join(current_dir, "list_comparator.py")
|
|
1762
|
+
|
|
1763
|
+
# Check if the list_comparator.py file exists
|
|
1764
|
+
if os.path.exists(list_comparator_path):
|
|
1765
|
+
# Launch the list comparator as a separate process without console window
|
|
1766
|
+
if sys.platform.startswith('win'):
|
|
1767
|
+
# Windows - use pythonw.exe to avoid console window, or hide it
|
|
1768
|
+
try:
|
|
1769
|
+
# Try to use pythonw.exe first (no console window)
|
|
1770
|
+
pythonw_path = sys.executable.replace('python.exe', 'pythonw.exe')
|
|
1771
|
+
if os.path.exists(pythonw_path):
|
|
1772
|
+
subprocess.Popen([pythonw_path, list_comparator_path])
|
|
1773
|
+
else:
|
|
1774
|
+
# Fallback: use regular python but hide the console window
|
|
1775
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
1776
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
1777
|
+
startupinfo.wShowWindow = subprocess.SW_HIDE
|
|
1778
|
+
subprocess.Popen([sys.executable, list_comparator_path],
|
|
1779
|
+
startupinfo=startupinfo)
|
|
1780
|
+
except Exception:
|
|
1781
|
+
# Final fallback
|
|
1782
|
+
subprocess.Popen([sys.executable, list_comparator_path])
|
|
1783
|
+
else:
|
|
1784
|
+
# Unix/Linux/macOS
|
|
1785
|
+
subprocess.Popen([sys.executable, list_comparator_path])
|
|
1786
|
+
|
|
1787
|
+
print("✅ List Comparator launched successfully (subprocess)")
|
|
1788
|
+
else:
|
|
1789
|
+
print(f"❌ List Comparator not found at: {list_comparator_path}")
|
|
1790
|
+
# Try to show a message to the user if possible
|
|
1791
|
+
self._show_warning("List Comparator",
|
|
1792
|
+
f"List Comparator application not found.\n\nExpected location: {list_comparator_path}")
|
|
1793
|
+
|
|
1794
|
+
except Exception as e:
|
|
1795
|
+
print(f"❌ Error launching List Comparator: {e}")
|
|
1796
|
+
self._show_error("List Comparator",
|
|
1797
|
+
f"Error launching List Comparator:\n{str(e)}")
|
|
1798
|
+
|
|
1799
|
+
def get_settings(self):
|
|
1800
|
+
"""Get current settings."""
|
|
1801
|
+
return {"option": self.option_var.get()}
|
|
1802
|
+
|
|
1803
|
+
def _show_warning(self, title, message):
|
|
1804
|
+
"""Show warning dialog using DialogManager if available, otherwise use messagebox."""
|
|
1805
|
+
if hasattr(self.diff_viewer, '_show_warning'):
|
|
1806
|
+
return self.diff_viewer._show_warning(title, message)
|
|
1807
|
+
else:
|
|
1808
|
+
messagebox.showwarning(title, message, parent=self.parent)
|
|
1809
|
+
return True
|
|
1810
|
+
|
|
1811
|
+
def _show_error(self, title, message):
|
|
1812
|
+
"""Show error dialog using DialogManager if available, otherwise use messagebox."""
|
|
1813
|
+
if hasattr(self.diff_viewer, '_show_error'):
|
|
1814
|
+
return self.diff_viewer._show_error(title, message)
|
|
1815
|
+
else:
|
|
1816
|
+
messagebox.showerror(title, message, parent=self.parent)
|
|
1817
|
+
return True
|