Skip to content

Commit af59c33

Browse files
feat: add replace feature (#444)
Co-authored-by: Eugenio Parodi <eugenio.parodi@sky.uk>
1 parent e7103c7 commit af59c33

1 file changed

Lines changed: 121 additions & 51 deletions

File tree

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

Lines changed: 121 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
import fnmatch
2828
import 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

3334
import TermTk as ttk
3435

@@ -134,17 +135,27 @@ def __init__(self, *args, match:str, **kwargs):
134135

135136
class 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}'\nin {_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

Comments
 (0)