Browse Source

feat: ttkode search plugin (#385)

pull/388/head
Pier CeccoPierangioliEugenio 11 months ago committed by GitHub
parent
commit
1d65bdfdfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 69
      apps/ttkode/ttkode/app/ttkode.py
  2. 225
      apps/ttkode/ttkode/plugins/_010/findwidget.py

69
apps/ttkode/ttkode/app/ttkode.py

@ -33,7 +33,7 @@ from TermTk import pyTTkSlot, pyTTkSignal
from TermTk import TTkFrame, TTkButton
from TermTk import TTkKodeTab
from TermTk import TTkFileDialogPicker
from TermTk import TTkFileTree, TTkTextEdit
from TermTk import TTkFileTree, TTkTextEdit, TTkTextCursor
from TermTk import TTkGridLayout
from TermTk import TTkSplitter,TTkAppTemplate
@ -42,10 +42,24 @@ from TermTk import TTkLogViewer
from TermTk import TTkMenuBarLayout
from TermTk import TTkAbout
from TermTk import TTkTestWidget, TTkTestWidgetSizes
from TermTk import TTkDnDEvent
from TermTk import TTkTreeWidget, TTkTreeWidgetItem, TTkFileTreeWidgetItem
from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData
from .about import About
from .activitybar import TTKodeActivityBar
class TTKodeFileWidgetItem(TTkTreeWidgetItem):
__slots__ = ('_path', '_lineNumber')
def __init__(self, *args, path:str, lineNumber:int=0, **kwargs) -> None:
self._path = path
self._lineNumber = lineNumber
super().__init__(*args, **kwargs)
def path(self) -> str:
return self._path
def lineNumber(self) -> int:
return self._lineNumber
class _TextDocument(TextDocumentHighlight):
__slots__ = ('_filePath')
def __init__(self, filePath:str="", **kwargs):
@ -64,6 +78,7 @@ class TTKode(TTkGridLayout):
self.addWidget(appTemplate)
self._kodeTab = TTkKodeTab(border=False, closable=True)
self._kodeTab.setDropEventProxy(self._dropEventProxyFile)
appTemplate.setMenuBar(appMenuBar:=TTkMenuBarLayout(), TTkAppTemplate.MAIN)
fileMenu = appMenuBar.addMenu("&File")
@ -81,7 +96,7 @@ class TTKode(TTkGridLayout):
helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout)
helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk)
fileTree = TTkFileTree(path='.')
fileTree = TTkFileTree(path='.', dragDropMode=TTkK.DragDropMode.AllowDrag)
self._activityBar = TTKodeActivityBar()
self._activityBar.addActivity(name="Explorer", icon=TTkString("╔██\n╚═╝"), widget=fileTree, select=True)
@ -100,7 +115,7 @@ class TTKode(TTkGridLayout):
filePicker.pathPicked.connect(self._openFile)
TTkHelper.overlay(None, filePicker, 20, 5, True)
def _openFile(self, filePath):
def _openFile(self, filePath, lineNumber=0):
filePath = os.path.realpath(filePath)
if filePath in self._documents:
doc = self._documents[filePath]['doc']
@ -115,6 +130,54 @@ class TTKode(TTkGridLayout):
self._kodeTab.addTab(tedit, label)
self._kodeTab.setCurrentWidget(tedit)
if lineNumber:
tedit.textCursor().movePosition(operation=TTkTextCursor.MoveOperation.End)
tedit.ensureCursorVisible()
tedit.textCursor().setPosition(line=lineNumber,pos=0)
tedit.ensureCursorVisible()
newCursor = tedit.textCursor().copy()
newCursor.clearSelection()
selection = TTkTextEdit.ExtraSelection(
cursor=newCursor,
color=TTkColor.bg("#444400"),
format=TTkK.SelectionFormat.FullWidthSelection)
tedit.setExtraSelections([selection])
tedit.setFocus()
def _dropEventProxyFile(self, evt:TTkDnDEvent):
data = evt.data()
filePath = None
if ( issubclass(type(data), TTkTreeWidget._DropTreeData) and
data.items ):
if issubclass(type(data.items[0]), TTkFileTreeWidgetItem):
item:TTkFileTreeWidgetItem = data.items[0]
filePath = os.path.realpath(item.path())
elif issubclass(type(data.items[0]), TTKodeFileWidgetItem):
item:TTkFileTreeWidgetItem = data.items[0]
filePath = os.path.realpath(item.path())
if filePath:
if filePath in self._documents:
doc = self._documents[filePath]['doc']
else:
with open(filePath, 'r') as f:
content = f.read()
doc = _TextDocument(text=content, filePath=filePath)
self._documents[filePath] = {'doc':doc,'tabs':[]}
tedit = TTkTextEdit(document=doc, readOnly=False, lineNumber=True)
label = TTkString(TTkCfg.theme.fileIcon.getIcon(filePath),TTkCfg.theme.fileIconColor) + TTkColor.RST + " " + os.path.basename(filePath)
newData = _TTkNewTabWidgetDragData(
widget=tedit,
label=label,
data=None,
closable=True
)
newEvt = evt.clone()
newEvt.setData(newData)
return newEvt
return evt
# def _closeFile():
# if (index := KodeTab.lastUsed.currentIndex()) >= 0:
# KodeTab.lastUsed.removeTab(index)

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

@ -22,21 +22,230 @@
__all__ = ['FindWidget']
import os
import re
import fnmatch
import mimetypes
from threading import Thread
from typing import Generator,List,Tuple
import TermTk as ttk
import ttkode
import os
import fnmatch
from ttkode import ttkodeProxy
from ttkode.app.ttkode import TTKodeFileWidgetItem
import mimetypes
def is_text_file(file_path, block_size=512):
# Check MIME type
mime_type, _ = mimetypes.guess_type(file_path)
text_based_mime_types = [
'text/', 'application/json', 'application/xml',
'application/javascript', 'application/x-httpd-php',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]
if mime_type is not None and any(mime_type.startswith(mime) for mime in text_based_mime_types):
return True
# Check for non-printable characters
try:
with open(file_path, 'rb') as file:
block = file.read(block_size)
if b'\0' in block:
return False
text_characters = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
return not bool(block.translate(None, text_characters))
except Exception as e:
print(f"Error reading file: {e}")
return False
def _load_gitignore_patterns(gitignore_path):
if os.path.exists(gitignore_path):
with open(gitignore_path, 'r') as f:
patterns = f.read().splitlines()
return patterns
return []
def _should_ignore(path, patterns):
for pattern in patterns:
if fnmatch.fnmatch(path, pattern):
return True
return False
def _custom_walk(directory:str, patterns:List[str]=[]) -> Generator[Tuple[str, str], None, None]:
gitignore_path = os.path.join(directory, '.gitignore')
patterns = patterns + _load_gitignore_patterns(gitignore_path)
for entry in sorted(os.listdir(directory)):
full_path = os.path.join(directory, entry)
if _should_ignore(full_path, patterns):
continue
if os.path.isdir(full_path):
if entry == '.git':
continue
yield from _custom_walk(full_path, patterns)
else:
yield directory, entry
def _walk_with_gitignore(root):
for dirpath, filenames in os.walk(root):
gitignore_path = os.path.join(dirpath, '.gitignore')
patterns = _load_gitignore_patterns(gitignore_path)
filenames[:] = [f for f in filenames if not _should_ignore(os.path.join(dirpath, f), patterns)]
yield dirpath, filenames
class _ExpandButton(ttk.TTkButton):
def __init__(self, **kwargs):
params = {
'border':False,
'checked':False,
'checkable':True,
'minSize':(4,1),
'maxSize':(4,1),
}
super().__init__(**kwargs|params)
def paintEvent(self, canvas):
if self.isChecked():
canvas.drawChar(pos=(1,0),char='')
else:
canvas.drawChar(pos=(1,0),char='')
class _MatchTreeWidgetItem(TTKodeFileWidgetItem):
__slots__ = ('_match','_line','_file')
_match:str
def __init__(self, *args, match:str, **kwargs):
self._match = match
super().__init__(*args, **kwargs)
class FindWidget(ttk.TTkContainer):
__slots__ = (
'_runId'
'_results_tree',
'_search_le','_replace_le','_files_inc_le','_files_exc_le')
_runId:int
_results_tree:ttk.TTkTreeWidget
_search_le:ttk.TTkLineEdit
_replace_le:ttk.TTkLineEdit
_files_inc_le:ttk.TTkLineEdit
_files_exc_le:ttk.TTkLineEdit
def __init__(self, **kwargs):
self._runId = 0
super().__init__(**kwargs)
self.setLayout(layout:=ttk.TTkGridLayout())
searchLayout = ttk.TTkGridLayout()
searchLayout.addWidget(expandReplace:=ttk.TTkButton(text=">", maxWidth=3, checkable=True), 0, 0)
searchLayout.addWidget(ttk.TTkLineEdit(), 0, 1)
searchLayout.addWidget(replace:=ttk.TTkLineEdit(), 1, 0, 1, 2)
searchLayout.addWidget(expandReplace:=_ExpandButton(), 0, 0)
searchLayout.addWidget(search :=ttk.TTkLineEdit(hint='Search'), 0, 1)
searchLayout.addWidget(repl__l:=ttk.TTkLabel(visible=False, text='sub:'), 1, 0)
searchLayout.addWidget(ft_in_l:=ttk.TTkLabel(visible=False, text='inc:'), 2, 0)
searchLayout.addWidget(ft_ex_l:=ttk.TTkLabel(visible=False, text='exc:'), 3, 0)
searchLayout.addWidget(replace:=ttk.TTkLineEdit(visible=False, hint='Replace'), 1, 1)
searchLayout.addWidget(ft_incl:=ttk.TTkLineEdit(visible=False, hint='Files to include'), 2, 1)
searchLayout.addWidget(ft_excl:=ttk.TTkLineEdit(visible=False, hint='Files to exclude'), 3, 1)
layout.addItem(searchLayout, 0, 0)
layout.addWidget(ttk.TTkButton(text="Find", border=False), 1,0)
layout.addWidget(ttk.TTkButton(text="Find", border=False), 2,0)
layout.addWidget(ttk.TTkButton(text="Find", border=True), 3,0)
layout.addWidget(btn_search:=ttk.TTkButton(text="Search", border=False), 4,0)
layout.addWidget(res_tree:=ttk.TTkTree(dragDropMode=ttk.TTkK.DragDropMode.AllowDrag), 5,0)
res_tree.setHeaderLabels(["Results"])
res_tree.setColumnWidth(0,100)
expandReplace.toggled.connect(replace.setVisible)
replace.setVisible(False)
expandReplace.toggled.connect(repl__l.setVisible)
expandReplace.toggled.connect(ft_incl.setVisible)
expandReplace.toggled.connect(ft_excl.setVisible)
expandReplace.toggled.connect(ft_in_l.setVisible)
expandReplace.toggled.connect(ft_ex_l.setVisible)
self._results_tree = res_tree
self._search_le = search
self._replace_le = replace
self._files_inc_le = ft_incl
self._files_exc_le = ft_excl
btn_search.clicked.connect(self._search)
search.returnPressed.connect(self._search)
res_tree.itemActivated.connect(self._activated)
@ttk.pyTTkSlot(ttk.TTkTreeWidgetItem, int)
def _activated(self, item:ttk.TTkTreeWidgetItem, _):
if isinstance(item, _MatchTreeWidgetItem):
file = item.path()
line = item.lineNumber()
ttkodeProxy.ttkode()._openFile(file, line)
@ttk.pyTTkSlot()
def _search(self):
self._runId += 1
search_pattern = str(self._search_le.text())
if not search_pattern:
return
def _search_threading():
self._results_tree.clear()
group = []
groupSize = 1
for (file,root,matches) in self._search_files('.',str(search_pattern),self._runId):
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.TTkString(f" {root} ", ttk.TTkColor.fg("#888888"))
],expanded=True)
for num,line in matches:
item.addChild(
_MatchTreeWidgetItem([
ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) +
ttk.TTkString(line.replace('\n','')).completeColor(
match=search_pattern,
color=ttk.TTkColor.GREEN)
],
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:
self._results_tree.addTopLevelItems(group)
Thread(target=_search_threading).start()
def _search_files(self, root_folder, match, runId):
matches = []
for root, file in _custom_walk(root_folder):
if runId != self._runId:
return
if True: # file.endswith('.py'): # file.endswith(file_extension):
file_path = os.path.join(root, file)
if not is_text_file(file_path):
continue
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
line_matches = [(i, line.split('\n')[0]) for i, line in enumerate(lines) if match in line]
if line_matches:
yield (file, root, line_matches)
def _search_files_re(self, root_folder, file_extension, search_pattern):
matches = []
regex = re.compile(search_pattern)
for root, dirs, files in os.walk(root_folder):
for file in files:
if True: # file.endswith(file_extension):
file_path = os.path.join(root, file)
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
line_matches = [i + 1 for i, line in enumerate(lines) if regex.search(line)]
if line_matches:
yield (file_path, line_matches)

Loading…
Cancel
Save