Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions visidata/cliptext.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,15 @@ def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clea
try:
for colorstate, chunk in chunks:
if colorstate:
if isinstance(colorstate, str):
cattr = cattr.update(colors.get_color(colorstate), 100)
if isinstance(colorstate, ColorAttr):
cattr = origattr.update(colorstate, 100)
elif isinstance(colorstate, str):
cattr = origattr.update(colors.get_color(colorstate), 100)
else:
cattr = origattr.update(colorstate['cattr'], 100)
link = colorstate['link']
else:
cattr = origattr

if not chunk:
continue
Expand Down
110 changes: 110 additions & 0 deletions visidata/features/hlsearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from visidata import Sheet, TableSheet, Column
from visidata import vd, dispwidth
import re


TableSheet.init('highlight_regex', lambda: None, copy=False)
Column.init('highlight_regex', lambda: None, copy=False)

@Sheet.api
def highlight_chunks(sheet, chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr):
'''Highlights *hp* which is a regex to highlight, or None if no highlighting.
Always handles formatting for *hoffset* even when *hp* is None.
Consumes the generator *chunks*. Returns a list of (attr, string) pairs.
'''
display_chunks = []
left_hl = False
right_hl = False
truncate_right = False #becomes True only if the highlighted chunk ends at the col edge
dispw = 0
for attr, text in chunks:
last = hoffset if hoffset > 0 else 0
# note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints
matches = re.finditer(hp, text) if hp else []
for m in matches:
m1 = m.start()
m2 = m.end()
if m1 < hoffset:
left_hl = True
if m2 <= hoffset:
continue
m1 = hoffset
if m1 > last:
s = text[last:m1]
if truncate_right:
display_chunks[-1][1] += s
else:
display_chunks.append([attr, s])
dispw += dispwidth(s)
s = text[m1:m2]
if truncate_right:
display_chunks[-1][1] += s
else:
display_chunks.append([hl_attr, s])
dispw += dispwidth(text[m1:m2])
if dispw > colwidth-notewidth-1:
right_hl = True
last = len(text)
break
if dispw == colwidth-notewidth-1:
#append any subsequent cell text to the highlighted chunk so it gets a truncator added by clipdraw()
truncate_right = True
last = m2
if last < len(text):
s = text[last:]
if truncate_right:
display_chunks[-1][1] += s
else:
display_chunks.append([attr, s])
dispw += dispwidth(s)
return display_chunks, left_hl, right_hl

@Sheet.api
def setHighlightRegex(sheet, r, cols=[]):
if not sheet.options.highlight_search:
return
flagbits = sum(getattr(re, f.upper()) for f in r['flags'])
rc = re.compile(r['regex'], flagbits)
sheet.clear_search()
if cols is None:
vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex)
sheet.highlight_regex = rc
else:
for col in cols:
vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex)
col.highlight_regex = rc

@Sheet.api
def highlight_input(sheet, cols=[]):
r = vd.inputMultiple(regex=dict(prompt=f"highlight regex: ", type="regex", defaultLast=True, help=vd.help_regex),
flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags))
if not sheet.options.highlight_search:
vd.warning('highlight option needs to be set to True')
sheet.setHighlightRegex(r, cols)

@Sheet.after
def clear_search(sheet):
if not sheet.options.highlight_search:
return
for col in sheet.columns:
if col.highlight_regex:
vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex)
col.highlight_regex = None
if sheet.highlight_regex:
vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex)
sheet.highlight_regex = None

vd.option('highlight_search', True, 'whether to highlight strings in searches')
vd.option('color_highlight_search', '21 blue on 15 white', 'color to use for highlighting search results', sheettype=None) #bright blue on white

Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns')
Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column')
Sheet.addCommand('', 'highlight-clear', 'clear_search()', 'clear the current highlight pattern')
# redefine existing commands
Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; cols=keyCols or [visibleCols[0]]; r=moveInputRegex("row key", type="regex-row", columns=cols); setHighlightRegex(r, cols); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex')
Sheet.addCommand('/', 'search-col', 'r=moveInputRegex("search", columns="cursorCol", backward=False); setHighlightRegex(r, [cursorCol])', 'search for regex forwards in current column')
Sheet.addCommand('?', 'searchr-col', 'r=moveInputRegex("reverse search", columns="cursorCol", backward=True); setHighlightRegex(r, [cursorCol])', 'search for regex backwards in current column')
Sheet.addCommand('g/', 'search-cols', 'r=moveInputRegex("g/", backward=False, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex forwards over all visible columns')
Sheet.addCommand('g?', 'searchr-cols', 'r=moveInputRegex("g?", backward=True, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex backwards over all visible columns')

vd.addGlobals(highlight_chunks=highlight_chunks)
9 changes: 7 additions & 2 deletions visidata/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def searchInputRegex(sheet, action:str, columns:str='cursorCol'):
def moveInputRegex(sheet, action:str, type="regex", **kwargs):
r = vd.inputMultiple(regex=dict(prompt=f"{action} regex: ", type=type, defaultLast=True, help=vd.help_regex),
flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags))

return vd.moveRegex(sheet, regex=r['regex'], regex_flags=r['flags'], **kwargs)
vd.moveRegex(sheet, regex=r['regex'], regex_flags=r['flags'], **kwargs)
return r

@Sheet.api
@asyncthread
Expand All @@ -102,6 +102,11 @@ def search_expr(sheet, expr, reverse=False, curcol=None):

vd.fail(f'no {sheet.rowtype} where {expr}')

@Sheet.api
def clear_search(sheet):
'''A stub function to clear any aftereffects of search, such as when
highlight_search is active.'''
pass

Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; moveInputRegex("row key", type="regex-row", columns=keyCols or [visibleCols[0]]); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex')
Sheet.addCommand('/', 'search-col', 'moveInputRegex("search", columns="cursorCol", backward=False)', 'search for regex forwards in current column')
Expand Down
30 changes: 20 additions & 10 deletions visidata/sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import itertools
from copy import copy, deepcopy
import textwrap
import re

from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException, Fanout
from visidata import (options, Column, namedlist, SettableColumn, AttrDict, DisplayWrapper,
Expand Down Expand Up @@ -1040,20 +1041,29 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight,
elif len(lines) < height:
lines.extend([[('', '')]]*(height-len(lines)))

for i, chunks in enumerate(lines):
if self.options.get('highlight_search', False):
hp = col.highlight_regex or self.highlight_regex
hl_attr = colors.color_highlight_search
else:
hp = None

for i, chunks in enumerate(lines): #chunks is a generator
y = ybase+i

sepchars = seps[i]

pre = disp_truncator if hoffset != 0 else disp_column_fill
prechunks = []
left_hl = right_hl = False
if hp: # chunks becomes a list
chunks, left_hl, right_hl = self.highlight_chunks(chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr)
else:
chunks = [(attr, text[hoffset:]) for attr, text in chunks]
if colwidth > 2:
prechunks.append(('', pre))

for attr, text in chunks:
prechunks.append((attr, text[hoffset:]))

clipdraw_chunks(scr, y, x, prechunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth)
pre = disp_truncator if hoffset != 0 else disp_column_fill
chunks.insert(0, (hl_attr if left_hl else cattr, pre))
clipdraw_chunks(scr, y, x, chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth)
if right_hl:
hl_attr = update_attr(cattr, hl_attr, 100)
clipdraw(scr, y, x+(colwidth-notewidth-1), disp_truncator, hl_attr, w=dispwidth(disp_truncator))
vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell')

if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth-1:
Expand Down Expand Up @@ -1310,7 +1320,7 @@ def reload_or_replace(sheet):
BaseSheet.addCommand('gTab', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen')
BaseSheet.addCommand('zZ', 'splitwin-input', 'vd.options.disp_splitwin_pct = input("% height for split window: ", value=vd.options.disp_splitwin_pct)', 'set split pane to specific size')

BaseSheet.addCommand('Ctrl+L', 'redraw', 'sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen')
BaseSheet.addCommand('Ctrl+L', 'redraw', 'clear_search(); sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen')
BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit')
BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit')
BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet')
Expand Down
2 changes: 2 additions & 0 deletions visidata/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def isTestableCommand(longname, cmdlist):
'row': '5',
'addcol-aggregate': 'max',
'define-command': 'type-test cursorCol.type = str',
'highlight-sheet': 'e..',
'highlight-col': '[0-9]',
}

@pytest.mark.usefixtures('curses_setup')
Expand Down