diff --git a/apps/ttkode/ttkode/plugins/_010/findwidget.py b/apps/ttkode/ttkode/plugins/_010/findwidget.py index 758eee75..53289f9c 100644 --- a/apps/ttkode/ttkode/plugins/_010/findwidget.py +++ b/apps/ttkode/ttkode/plugins/_010/findwidget.py @@ -27,8 +27,9 @@ import re import fnmatch import mimetypes -from threading import Thread -from typing import Generator,List,Tuple +from pathlib import Path +from threading import Thread,Event,Lock +from typing import Generator,List,Tuple,Dict,Optional import TermTk as ttk @@ -134,10 +135,16 @@ class _MatchTreeWidgetItem(TTKodeFileWidgetItem): class FindWidget(ttk.TTkContainer): __slots__ = ( - '_runId' + '_runId', + '_replace_data', '_results_tree', + '_search_thread', '_search_stop_event', '_search_lock', '_search_le','_replace_le','_files_inc_le','_files_exc_le') _runId:int + _search_lock:Lock + _search_thread:Optional[Thread] + _search_stop_event:Event + _replace_data:Dict _results_tree:ttk.TTkTreeWidget _search_le:ttk.TTkLineEdit _replace_le:ttk.TTkLineEdit @@ -145,6 +152,10 @@ class FindWidget(ttk.TTkContainer): _files_exc_le:ttk.TTkLineEdit def __init__(self, **kwargs): self._runId = 0 + self._replace_data = {} + self._search_thread = None + self._search_lock = Lock() + self._search_stop_event = Event() super().__init__(**kwargs) self.setLayout(layout:=ttk.TTkGridLayout()) @@ -185,12 +196,20 @@ class FindWidget(ttk.TTkContainer): self._files_exc_le = ft_excl btn_search.clicked.connect(self._search) + btn_replace.clicked.connect(self._ask_replace) btn_expand.clicked.connect(self._results_tree.expandAll) btn_collapse.clicked.connect(self._results_tree.collapseAll) + search.returnPressed.connect(self._search) replace.returnPressed.connect(self._search) ft_incl.returnPressed.connect(self._search) ft_excl.returnPressed.connect(self._search) + + search.textEdited.connect(self._search) + replace.textEdited.connect(self._search) + ft_incl.textEdited.connect(self._search) + ft_excl.textEdited.connect(self._search) + res_tree.itemActivated.connect(self._activated) @ttk.pyTTkSlot(str) @@ -207,58 +226,109 @@ class FindWidget(ttk.TTkContainer): line = item.lineNumber() ttkodeProxy.openFile(file, line) - @ttk.pyTTkSlot() - def _search(self): - self._runId += 1 - search_pattern = str(self._search_le.text()) - replace_pattern = str(self._replace_le.text()) - include_patterns = _s.split(',') if (_s:=str(self._files_inc_le.text())) else [] - exclude_patterns = _s.split(',') if (_s:=str(self._files_exc_le.text())) else [] - if not search_pattern: + def _ask_replace(self) -> None: + if not self._replace_le.text(): return - def _search_threading(): - self._results_tree.clear() - group = [] - groupSize = 1 - for (file,root,matches) in self._search_files('.',str(search_pattern),self._runId,include_patterns,exclude_patterns): - # ttk.TTkLog.debug((file,matches)) - item = ttk.TTkTreeWidgetItem([ - ttk.TTkString( - ttk.TTkCfg.theme.fileIcon.getIcon(file), - ttk.TTkCfg.theme.fileIconColor) + " " + - ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD+ttk.TTkColor.bg('#000088')) + - ttk.TTkString(f" {root} ", ttk.TTkColor.fg("#888888")) - ],expanded=True) - for num,line in matches: - line = line.lstrip(' ') - # index = line.find(search_pattern) - # outLine = - if replace_pattern: - _s = line.replace('\n','').split(search_pattern) - _j = ( - ttk.TTkString(search_pattern,ttk.TTkColor.RED + ttk.TTkColor.STRIKETROUGH) + - ttk.TTkString(replace_pattern,ttk.TTkColor.GREEN) + ttk.TTkColor.RST) - ttkLine = _j.join(_s) + _numFiles = len(self._replace_data.get('files',[])) + _numMatches = sum(len(_r.get('matches',[])) for _r in self._replace_data.get('files',[])) + _search_pattern = str(self._search_le.text()) + _replace_pattern = str(self._replace_le.text()) + + messageBox = ttk.TTkMessageBox( + title="🚨 Apply? 🚨", + text=ttk.TTkString(f"Do you want to repace {_numMatches} occurrences of\n'{_search_pattern}'\nin {_numFiles} files with\n'{_replace_pattern}'?"), + icon=ttk.TTkMessageBox.Icon.Warning, + standardButtons= + ttk.TTkMessageBox.StandardButton.Ok| + ttk.TTkMessageBox.StandardButton.Cancel) + + @ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton) + def _cb(btn): + if btn == ttk.TTkMessageBox.StandardButton.Ok: + ttk.TTkLog.debug(f"Replace '{_search_pattern}' with '{_replace_pattern}'") + for _file_def in self._replace_data.get('files',[]): + _file = Path(_file_def['root']) / _file_def['file'] + if not self._replace_data.get('files',[]): + ttk.TTkLog.error(f"{_file} does not exists!!!") else: - ttkLine = ttk.TTkString(line.replace('\n','')).completeColor( - match=search_pattern, - color=ttk.TTkColor.GREEN) - - item.addChild( - _MatchTreeWidgetItem([ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) + ttkLine] , - match=line, - lineNumber=num, - path=os.path.join(root,file))) - group.append(item) - if len(group) > groupSize: - self._results_tree.addTopLevelItems(group) - group = [] - groupSize <<= 1 - # self._results_tree.addTopLevelItem(item) - if group: + _content = _file.read_text() + _new_content = _content.replace(_search_pattern,_replace_pattern) + _file.write_text(_new_content) + else: + ttk.TTkLog.debug(f"Discard") + messageBox.buttonSelected.connect(_cb) + ttk.TTkHelper.overlay(None, messageBox, 5, 5, True) + + def _search_threading(self, search_pattern:str, include_patterns:str, exclude_patterns:str, replace_pattern:str) -> None: + self._results_tree.clear() + group = [] + groupSize = 1 + self._replace_data = {'files':[]} + for (file,root,matches) in self._search_files('.',search_pattern,self._runId,include_patterns,exclude_patterns): + if self._search_stop_event.is_set(): + break + + self._replace_data['files'].append({'file':file,'root':root,'matches':matches}) + # ttk.TTkLog.debug((file,matches)) + item = ttk.TTkTreeWidgetItem([ + ttk.TTkString( + ttk.TTkCfg.theme.fileIcon.getIcon(file), + ttk.TTkCfg.theme.fileIconColor) + " " + + ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD+ttk.TTkColor.bg('#000088')) + + ttk.TTkString(f" {root} ", ttk.TTkColor.fg("#888888")) + ],expanded=True) + for num,line in matches: + line = line.lstrip(' ') + # index = line.find(search_pattern) + # outLine = + if replace_pattern: + _s = line.replace('\n','').split(search_pattern) + _j = ( + ttk.TTkString(search_pattern,ttk.TTkColor.RED + ttk.TTkColor.STRIKETROUGH) + + ttk.TTkString(replace_pattern,ttk.TTkColor.GREEN) + ttk.TTkColor.RST) + ttkLine = _j.join(_s) + else: + ttkLine = ttk.TTkString(line.replace('\n','')).completeColor( + match=search_pattern, + color=ttk.TTkColor.GREEN) + + item.addChild( + _MatchTreeWidgetItem([ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) + ttkLine] , + match=line, + lineNumber=num, + path=os.path.join(root,file))) + group.append(item) + if len(group) > groupSize: self._results_tree.addTopLevelItems(group) - Thread(target=_search_threading).start() + group = [] + groupSize <<= 1 + # self._results_tree.addTopLevelItem(item) + if group: + self._results_tree.addTopLevelItems(group) + + @ttk.pyTTkSlot() + def _search(self) -> None: + with self._search_lock: + if self._search_thread: + self._search_stop_event.set() + self._search_thread.join() + self._search_stop_event.clear() + self._search_thread = None + self._runId += 1 + search_pattern = str(self._search_le.text()) + replace_pattern = str(self._replace_le.text()) + include_patterns = _s.split(',') if (_s:=str(self._files_inc_le.text())) else [] + exclude_patterns = _s.split(',') if (_s:=str(self._files_exc_le.text())) else [] + if not search_pattern: + self._results_tree.clear() + return + if self._search_thread: + self._search_thread.join() + self._search_thread = Thread( + target=self._search_threading, + args=(search_pattern, include_patterns, exclude_patterns, replace_pattern)) + self._search_thread.start() def _search_files(self, root_folder, match, runId, include_patterns, exclude_patterns): for root, file in _custom_walk(root_folder,include_patterns,exclude_patterns):