2727import fnmatch
2828import mimetypes
2929
30- from threading import Thread
31- from typing import Generator ,List ,Tuple
30+ from pathlib import Path
31+ from threading import Thread ,Event ,Lock
32+ from typing import Generator ,List ,Tuple ,Dict ,Optional
3233
3334import TermTk as ttk
3435
@@ -134,17 +135,27 @@ def __init__(self, *args, match:str, **kwargs):
134135
135136class FindWidget (ttk .TTkContainer ):
136137 __slots__ = (
137- '_runId'
138+ '_runId' ,
139+ '_replace_data' ,
138140 '_results_tree' ,
141+ '_search_thread' , '_search_stop_event' , '_search_lock' ,
139142 '_search_le' ,'_replace_le' ,'_files_inc_le' ,'_files_exc_le' )
140143 _runId :int
144+ _search_lock :Lock
145+ _search_thread :Optional [Thread ]
146+ _search_stop_event :Event
147+ _replace_data :Dict
141148 _results_tree :ttk .TTkTreeWidget
142149 _search_le :ttk .TTkLineEdit
143150 _replace_le :ttk .TTkLineEdit
144151 _files_inc_le :ttk .TTkLineEdit
145152 _files_exc_le :ttk .TTkLineEdit
146153 def __init__ (self , ** kwargs ):
147154 self ._runId = 0
155+ self ._replace_data = {}
156+ self ._search_thread = None
157+ self ._search_lock = Lock ()
158+ self ._search_stop_event = Event ()
148159 super ().__init__ (** kwargs )
149160 self .setLayout (layout := ttk .TTkGridLayout ())
150161
@@ -185,12 +196,20 @@ def __init__(self, **kwargs):
185196 self ._files_exc_le = ft_excl
186197
187198 btn_search .clicked .connect (self ._search )
199+ btn_replace .clicked .connect (self ._ask_replace )
188200 btn_expand .clicked .connect (self ._results_tree .expandAll )
189201 btn_collapse .clicked .connect (self ._results_tree .collapseAll )
202+
190203 search .returnPressed .connect (self ._search )
191204 replace .returnPressed .connect (self ._search )
192205 ft_incl .returnPressed .connect (self ._search )
193206 ft_excl .returnPressed .connect (self ._search )
207+
208+ search .textEdited .connect (self ._search )
209+ replace .textEdited .connect (self ._search )
210+ ft_incl .textEdited .connect (self ._search )
211+ ft_excl .textEdited .connect (self ._search )
212+
194213 res_tree .itemActivated .connect (self ._activated )
195214
196215 @ttk .pyTTkSlot (str )
@@ -207,58 +226,109 @@ def _activated(self, item:ttk.TTkTreeWidgetItem, _):
207226 line = item .lineNumber ()
208227 ttkodeProxy .openFile (file , line )
209228
210-
211229 @ttk .pyTTkSlot ()
212- def _search (self ):
213- self ._runId += 1
214- search_pattern = str (self ._search_le .text ())
215- replace_pattern = str (self ._replace_le .text ())
216- include_patterns = _s .split (',' ) if (_s := str (self ._files_inc_le .text ())) else []
217- exclude_patterns = _s .split (',' ) if (_s := str (self ._files_exc_le .text ())) else []
218- if not search_pattern :
230+ def _ask_replace (self ) -> None :
231+ if not self ._replace_le .text ():
219232 return
220- def _search_threading ():
221- self ._results_tree .clear ()
222- group = []
223- groupSize = 1
224- for (file ,root ,matches ) in self ._search_files ('.' ,str (search_pattern ),self ._runId ,include_patterns ,exclude_patterns ):
225- # ttk.TTkLog.debug((file,matches))
226- item = ttk .TTkTreeWidgetItem ([
227- ttk .TTkString (
228- ttk .TTkCfg .theme .fileIcon .getIcon (file ),
229- ttk .TTkCfg .theme .fileIconColor ) + " " +
230- ttk .TTkString (f" { file } " , ttk .TTkColor .YELLOW + ttk .TTkColor .BOLD + ttk .TTkColor .bg ('#000088' )) +
231- ttk .TTkString (f" { root } " , ttk .TTkColor .fg ("#888888" ))
232- ],expanded = True )
233- for num ,line in matches :
234- line = line .lstrip (' ' )
235- # index = line.find(search_pattern)
236- # outLine =
237- if replace_pattern :
238- _s = line .replace ('\n ' ,'' ).split (search_pattern )
239- _j = (
240- ttk .TTkString (search_pattern ,ttk .TTkColor .RED + ttk .TTkColor .STRIKETROUGH ) +
241- ttk .TTkString (replace_pattern ,ttk .TTkColor .GREEN ) + ttk .TTkColor .RST )
242- ttkLine = _j .join (_s )
233+ _numFiles = len (self ._replace_data .get ('files' ,[]))
234+ _numMatches = sum (len (_r .get ('matches' ,[])) for _r in self ._replace_data .get ('files' ,[]))
235+ _search_pattern = str (self ._search_le .text ())
236+ _replace_pattern = str (self ._replace_le .text ())
237+
238+ messageBox = ttk .TTkMessageBox (
239+ title = "🚨 Apply? 🚨" ,
240+ text = ttk .TTkString (f"Do you want to repace { _numMatches } occurrences of\n '{ _search_pattern } '\n in { _numFiles } files with\n '{ _replace_pattern } '?" ),
241+ icon = ttk .TTkMessageBox .Icon .Warning ,
242+ standardButtons =
243+ ttk .TTkMessageBox .StandardButton .Ok |
244+ ttk .TTkMessageBox .StandardButton .Cancel )
245+
246+ @ttk .pyTTkSlot (ttk .TTkMessageBox .StandardButton )
247+ def _cb (btn ):
248+ if btn == ttk .TTkMessageBox .StandardButton .Ok :
249+ ttk .TTkLog .debug (f"Replace '{ _search_pattern } ' with '{ _replace_pattern } '" )
250+ for _file_def in self ._replace_data .get ('files' ,[]):
251+ _file = Path (_file_def ['root' ]) / _file_def ['file' ]
252+ if not self ._replace_data .get ('files' ,[]):
253+ ttk .TTkLog .error (f"{ _file } does not exists!!!" )
243254 else :
244- ttkLine = ttk .TTkString (line .replace ('\n ' ,'' )).completeColor (
245- match = search_pattern ,
246- color = ttk .TTkColor .GREEN )
247-
248- item .addChild (
249- _MatchTreeWidgetItem ([ttk .TTkString (str (num )+ " " ,ttk .TTkColor .CYAN ) + ttkLine ] ,
250- match = line ,
251- lineNumber = num ,
252- path = os .path .join (root ,file )))
253- group .append (item )
254- if len (group ) > groupSize :
255- self ._results_tree .addTopLevelItems (group )
256- group = []
257- groupSize <<= 1
258- # self._results_tree.addTopLevelItem(item)
259- if group :
255+ _content = _file .read_text ()
256+ _new_content = _content .replace (_search_pattern ,_replace_pattern )
257+ _file .write_text (_new_content )
258+ else :
259+ ttk .TTkLog .debug (f"Discard" )
260+ messageBox .buttonSelected .connect (_cb )
261+ ttk .TTkHelper .overlay (None , messageBox , 5 , 5 , True )
262+
263+ def _search_threading (self , search_pattern :str , include_patterns :str , exclude_patterns :str , replace_pattern :str ) -> None :
264+ self ._results_tree .clear ()
265+ group = []
266+ groupSize = 1
267+ self ._replace_data = {'files' :[]}
268+ for (file ,root ,matches ) in self ._search_files ('.' ,search_pattern ,self ._runId ,include_patterns ,exclude_patterns ):
269+ if self ._search_stop_event .is_set ():
270+ break
271+
272+ self ._replace_data ['files' ].append ({'file' :file ,'root' :root ,'matches' :matches })
273+ # ttk.TTkLog.debug((file,matches))
274+ item = ttk .TTkTreeWidgetItem ([
275+ ttk .TTkString (
276+ ttk .TTkCfg .theme .fileIcon .getIcon (file ),
277+ ttk .TTkCfg .theme .fileIconColor ) + " " +
278+ ttk .TTkString (f" { file } " , ttk .TTkColor .YELLOW + ttk .TTkColor .BOLD + ttk .TTkColor .bg ('#000088' )) +
279+ ttk .TTkString (f" { root } " , ttk .TTkColor .fg ("#888888" ))
280+ ],expanded = True )
281+ for num ,line in matches :
282+ line = line .lstrip (' ' )
283+ # index = line.find(search_pattern)
284+ # outLine =
285+ if replace_pattern :
286+ _s = line .replace ('\n ' ,'' ).split (search_pattern )
287+ _j = (
288+ ttk .TTkString (search_pattern ,ttk .TTkColor .RED + ttk .TTkColor .STRIKETROUGH ) +
289+ ttk .TTkString (replace_pattern ,ttk .TTkColor .GREEN ) + ttk .TTkColor .RST )
290+ ttkLine = _j .join (_s )
291+ else :
292+ ttkLine = ttk .TTkString (line .replace ('\n ' ,'' )).completeColor (
293+ match = search_pattern ,
294+ color = ttk .TTkColor .GREEN )
295+
296+ item .addChild (
297+ _MatchTreeWidgetItem ([ttk .TTkString (str (num )+ " " ,ttk .TTkColor .CYAN ) + ttkLine ] ,
298+ match = line ,
299+ lineNumber = num ,
300+ path = os .path .join (root ,file )))
301+ group .append (item )
302+ if len (group ) > groupSize :
260303 self ._results_tree .addTopLevelItems (group )
261- Thread (target = _search_threading ).start ()
304+ group = []
305+ groupSize <<= 1
306+ # self._results_tree.addTopLevelItem(item)
307+ if group :
308+ self ._results_tree .addTopLevelItems (group )
309+
310+ @ttk .pyTTkSlot ()
311+ def _search (self ) -> None :
312+ with self ._search_lock :
313+ if self ._search_thread :
314+ self ._search_stop_event .set ()
315+ self ._search_thread .join ()
316+ self ._search_stop_event .clear ()
317+ self ._search_thread = None
318+ self ._runId += 1
319+ search_pattern = str (self ._search_le .text ())
320+ replace_pattern = str (self ._replace_le .text ())
321+ include_patterns = _s .split (',' ) if (_s := str (self ._files_inc_le .text ())) else []
322+ exclude_patterns = _s .split (',' ) if (_s := str (self ._files_exc_le .text ())) else []
323+ if not search_pattern :
324+ self ._results_tree .clear ()
325+ return
326+ if self ._search_thread :
327+ self ._search_thread .join ()
328+ self ._search_thread = Thread (
329+ target = self ._search_threading ,
330+ args = (search_pattern , include_patterns , exclude_patterns , replace_pattern ))
331+ self ._search_thread .start ()
262332
263333 def _search_files (self , root_folder , match , runId , include_patterns , exclude_patterns ):
264334 for root , file in _custom_walk (root_folder ,include_patterns ,exclude_patterns ):
0 commit comments