Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Commandpalette Proof of Concept #2059

Merged
merged 20 commits into from
Oct 23, 2023
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
1 change: 1 addition & 0 deletions visidata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def getGlobals():

import visidata.theme
import visidata.apps
import visidata.fuzzymatch
'''.splitlines():
if not line: continue
assert line.startswith('import visidata.'), line
Expand Down
2 changes: 1 addition & 1 deletion visidata/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,4 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
return r


vd.addGlobals({'CompleteKey': CompleteKey})
vd.addGlobals({'CompleteKey': CompleteKey, 'AcceptInput': AcceptInput})
14 changes: 0 additions & 14 deletions visidata/cmdlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,6 @@ def fnSuffix(vd, prefix):

return fn

@BaseSheet.api
def inputLongname(sheet):
longnames = set(k for (k, obj), v in vd.commands.iter(sheet))
return vd.input("command name: ", completer=CompleteKey(sorted(longnames)), type='longname')

@BaseSheet.api
def exec_longname(sheet, longname):
if not sheet.getCommand(longname):
vd.warning(f'no command {longname}')
return
sheet.execCommand(longname)

def indexMatch(L, func):
'returns the smallest i for which func(L[i]) is true'
for i, x in enumerate(L):
Expand Down Expand Up @@ -471,8 +459,6 @@ def repeat_for_selected(cmdlog, r):
globalCommand('^V', 'show-version', 'status(__version_info__);', 'Show version and copyright information on status line')
globalCommand('z^V', 'check-version', 'checkVersion(input("require version: ", value=__version_info__))', 'check VisiData version against given version')

BaseSheet.addCommand('Space', 'exec-longname', 'exec_longname(inputLongname())', 'execute command by its longname')

CommandLog.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
CommandLog.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')

Expand Down
107 changes: 107 additions & 0 deletions visidata/features/cmdpalette.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import collections
from visidata import BaseSheet, vd, CompleteKey, clipdraw, HelpSheet, colors, AcceptInput


vd.option('color_cmdpalette', 'black on 72', 'base color of command palette')
vd.option('disp_cmdpal_max', 5, 'max number of suggestions for command palette')


def _longname_executor(name):
def _exec(v, i):
raise AcceptInput(name)
return _exec


def _format_match(s, positions):
out = list(s)
for p in positions:
out[p] = f'[:bold]{out[p]}[/]'
return "".join(out)


def _fuzzymatch(longname, description, words):
longname_score, desc_score = 0, 0
positions_name = set()
positions_desc = set()
for word in words:
result_name = vd.fuzzymatch(longname, word)
result_desc = vd.fuzzymatch(description.lower(), word)
# if a word matches neither, we can skip the rest
if result_name.start == -1 and result_desc.start == -1:
longname_score, desc_score = 0, 0
break
longname_score = longname_score + result_name.score
desc_score = desc_score + result_desc.score
if result_name.positions:
positions_name.update(result_name.positions)
if result_desc.positions:
positions_desc.update(result_desc.positions)
# prefer if match is either fully on longname or on description
score = longname_score ** 2 + desc_score ** 2
return score, positions_desc, positions_name


@BaseSheet.api
def inputLongname(sheet):
label = 'command name: '
# get set of commands possible in the sheet
longnames = set(k for (k, obj), v in vd.commands.iter(sheet))
this_sheets_help = HelpSheet('', source=sheet)
this_sheets_help.ensureLoaded()
Match = collections.namedtuple('Match', 'name formatted_name keystrokes description score')
bindings = dict()

def cmdpal_matcher(value):
# collect data
matches = []
words = value.lower().split()
for row in this_sheets_help.rows:
description = this_sheets_help.cmddict[(row.sheet, row.longname)].helpstr
score, positions_desc, positions_name = _fuzzymatch(row.longname, description, words)
if score > 0:
keystrokes = this_sheets_help.revbinds.get(row.longname, [None])[0]
formatted_name = f'[:onclick {row.longname}]{_format_match(row.longname, positions_name)}[/]'
formatted_desc = _format_match(description, positions_desc)
matches.append(Match(row.longname, formatted_name, keystrokes, formatted_desc, score))
matches.sort(key=lambda m: -m.score)

# do the drawing
h, w = sheet._scr.getmaxyx()
cmdpal_h = min(h-2, vd.options.disp_cmdpal_max)
m_max = min(len(matches), cmdpal_h)

for i, match in enumerate(matches[:m_max]):
if i < 9:
trigger_key = f'{i+1}'
bindings[trigger_key] = _longname_executor(match.name)
else:
trigger_key = ' '
buffer = ' '*(len(label)-2)
# TODO: put keystrokes into buffer
match_summary = f'{buffer}[:keystrokes]{trigger_key}[/] {match.formatted_name}'
if match.keystrokes:
match_summary += f' ([:keystrokes]{match.keystrokes}[/])'
if match.description:
match_summary += f' - {match.description}'
if vd.options.debug:
debug_info = f'[{match.score}]'
match_summary = debug_info + match_summary[len(debug_info):]
clipdraw(sheet._scr, h-(m_max+1)+i, 0, match_summary, colors.color_cmdpalette, w=120)

# add some empty rows for visual appeal and dropping previous (not-anymore-)matches
for i in range(cmdpal_h - m_max):
clipdraw(sheet._scr, h-(cmdpal_h+1)+i, 0, ' ', colors.color_cmdpalette, w=120)

return None

return vd.input(label, completer=CompleteKey(sorted(longnames)), type='longname', updater=cmdpal_matcher,
bindings=bindings)

@BaseSheet.api
def exec_longname(sheet, longname):
if not sheet.getCommand(longname):
vd.fail(f'no command {longname}')
sheet.execCommand(longname)


vd.addCommand('Space', 'exec-longname', 'exec_longname(inputLongname())', 'execute command by its longname')
Loading
Loading