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