Browse Source

feat: add replace feature (#444)

Co-authored-by: Eugenio Parodi <eugenio.parodi@sky.uk>
pull/450/head
Pier CeccoPierangioliEugenio 7 months ago committed by GitHub
parent
commit
af59c3391f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 172
      apps/ttkode/ttkode/plugins/_010/findwidget.py

172
apps/ttkode/ttkode/plugins/_010/findwidget.py

@ -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):

Loading…
Cancel
Save