menuconfig: Make search more flexible and search prompts

This commit gets the following incremental search improvements in from
upstream:

  - 1d3db5de9b8c2 ("menuconfig: Add search with multiple search
    strings")

    This makes a search string like 'foo bar' match all symbol names
    that match both 'foo' and 'bar' (which can be regexes), regardless
    of the order in which they appear in the match. This is faster and
    more flexible than having to type a bunch of '.*'.

  - 9bf8fc6e6907e ("menuconfig: Add prompts to incremental search")

    This makes the incremental searcher search prompt strings as well as
    symbol names.

    The prompt is now displayed next to the symbol name in the list of
    matches as well.

Signed-off-by: Ulf Magnusson <ulfalizer@gmail.com>
This commit is contained in:
Ulf Magnusson 2018-05-11 00:22:32 +02:00 committed by Anas Nashif
commit e24788eb71

View file

@ -141,9 +141,10 @@ _INFO_HELP_LINES = """
# Lines of help text shown at the bottom of the search dialog # Lines of help text shown at the bottom of the search dialog
_JUMP_TO_HELP_LINES = """ _JUMP_TO_HELP_LINES = """
Type text to narrow the search. Regular expressions are supported (anything Type text to narrow the search. Regexes are supported (via Python's 're'
available in the Python 're' module). Use the up/down cursor keys to step in module). The up/down cursor keys step in the list. [Enter] jumps to the
the list. [Enter] jumps to the selected symbol. [ESC] aborts the search. selected symbol. [ESC] aborts the search. Type multiple space-separated
strings/regexes to find entries that match all of them.
"""[1:-1].split("\n") """[1:-1].split("\n")
def _init_styles(): def _init_styles():
@ -213,30 +214,30 @@ def _init_styles():
# Main application # Main application
# #
# Color pairs we've already created, indexed by a # color_attribs holds the color pairs we've already created, indexed by a
# (<foreground color>, <background color>) tuple # (<foreground color>, <background color>) tuple.
_color_attribs = {} #
# Obscure Python: We never pass a value for color_attribs, and it keeps
def _style(fg_color, bg_color, attribs, no_color_extra_attribs=0): # pointing to the same dict. This avoids a global.
def _style(fg_color, bg_color, attribs, no_color_extra_attribs=0,
color_attribs={}):
# Returns an attribute with the specified foreground and background color # Returns an attribute with the specified foreground and background color
# and the attributes in 'attribs'. Reuses color pairs already created if # and the attributes in 'attribs'. Reuses color pairs already created if
# possible, and creates a new color pair otherwise. # possible, and creates a new color pair otherwise.
# #
# Returns 'attribs | no_color_extra_attribs' if colors aren't supported. # Returns 'attribs | no_color_extra_attribs' if colors aren't supported.
global _color_attribs
if not curses.has_colors(): if not curses.has_colors():
return attribs | no_color_extra_attribs return attribs | no_color_extra_attribs
if (fg_color, bg_color) not in _color_attribs: if (fg_color, bg_color) not in color_attribs:
# Create new color pair. Color pair number 0 is hardcoded and cannot be # Create new color pair. Color pair number 0 is hardcoded and cannot be
# changed, hence the +1s. # changed, hence the +1s.
curses.init_pair(len(_color_attribs) + 1, fg_color, bg_color) curses.init_pair(len(color_attribs) + 1, fg_color, bg_color)
_color_attribs[(fg_color, bg_color)] = \ color_attribs[(fg_color, bg_color)] = \
curses.color_pair(len(_color_attribs) + 1) curses.color_pair(len(color_attribs) + 1)
return _color_attribs[(fg_color, bg_color)] | attribs return color_attribs[(fg_color, bg_color)] | attribs
# "Extend" the standard kconfiglib.expr_str() to show values for symbols # "Extend" the standard kconfiglib.expr_str() to show values for symbols
# appearing in expressions, for the information dialog. # appearing in expressions, for the information dialog.
@ -292,17 +293,21 @@ def menuconfig(kconf):
if _config_filename is None: if _config_filename is None:
_config_filename = ".config" _config_filename = ".config"
if os.path.exists(_config_filename): if os.path.exists(_config_filename):
print("Using existing configuration '{}' as base" print("Using existing configuration '{}' as base"
.format(_config_filename)) .format(_config_filename))
_kconf.load_config(_config_filename) _kconf.load_config(_config_filename)
elif kconf.defconfig_filename is not None: elif kconf.defconfig_filename is not None:
print("Using default configuration found in '{}' as base" print("Using default configuration found in '{}' as base"
.format(kconf.defconfig_filename)) .format(kconf.defconfig_filename))
_kconf.load_config(kconf.defconfig_filename) _kconf.load_config(kconf.defconfig_filename)
else: else:
print("Using default symbol values as base") print("Using default symbol values as base")
# Any visible items in the top menu? # Any visible items in the top menu?
_show_all = False _show_all = False
if not _shown_nodes(_kconf.top_node): if not _shown_nodes(_kconf.top_node):
@ -459,9 +464,15 @@ def _menuconfig(stdscr):
elif c == "/": elif c == "/":
_jump_to_dialog() _jump_to_dialog()
# The terminal might have been resized while the fullscreen jump-to
# dialog was open
_resize_main()
elif c == "?": elif c == "?":
_info_dialog(_shown[_sel_node_i]) _info_dialog(_shown[_sel_node_i])
# The terminal might have been resized while the fullscreen info
# dialog was open
_resize_main()
elif c in ("a", "A"): elif c in ("a", "A"):
_toggle_show_all() _toggle_show_all()
@ -912,7 +923,7 @@ def _draw_main():
def _parent_menu(node): def _parent_menu(node):
# Returns the menu node of the menu that contains 'node'. In addition to # Returns the menu node of the menu that contains 'node'. In addition to
# proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'.
# "Menu" here means a menu in the interface (a list of menu entries). # "Menu" here means a menu in the interface.
menu = node.parent menu = node.parent
while not menu.is_menuconfig: while not menu.is_menuconfig:
@ -1320,9 +1331,6 @@ def _jump_to_dialog():
_safe_curs_set(2) _safe_curs_set(2)
# Defined symbols sorted by name, with duplicates removed
sorted_syms = sorted(set(_kconf.defined_syms), key=lambda sym: sym.name)
# TODO: Code duplication with _select_{next,prev}_menu_entry(). Can this be # TODO: Code duplication with _select_{next,prev}_menu_entry(). Can this be
# factored out in some nice way? # factored out in some nice way?
@ -1353,20 +1361,23 @@ def _jump_to_dialog():
prev_s = s prev_s = s
try: try:
re_search = re.compile(s, re.IGNORECASE).search regex_searches = [re.compile(regex, re.IGNORECASE).search
for regex in s.split()]
# No exception thrown, so the regex is okay # No exception thrown, so the regexes are okay
bad_re = None bad_re = None
# 'matches' holds a list of matching menu nodes. # List of (node, node_string) tuples for the matching nodes
matches = []
# This is a bit faster than the loop equivalent. At a high # Go through the list of (node, node_string) tuples, where
# level, the syntax of list comprehensions is # 'node_string' describes 'node'
# [<item> <loop template>]. for node, node_string in _search_strings():
matches = [node for search in regex_searches:
for sym in sorted_syms if not search(node_string):
if re_search(sym.name) break
for node in sym.nodes] else:
matches.append((node, node_string))
except re.error as e: except re.error as e:
# Bad regex. Remember the error message so we can show it. # Bad regex. Remember the error message so we can show it.
@ -1385,27 +1396,17 @@ def _jump_to_dialog():
c = _get_wch_compat(edit_box) c = _get_wch_compat(edit_box)
if c == "\n": if c == "\n":
if not matches: if matches:
continue _jump_to(matches[sel_node_i][0])
_safe_curs_set(0)
_jump_to(matches[sel_node_i]) return
_safe_curs_set(0)
# Resize the main display before returning in case the terminal was
# resized while the search dialog was open
_resize_main()
return
if c == "\x1B": # \x1B = ESC if c == "\x1B": # \x1B = ESC
_safe_curs_set(0) _safe_curs_set(0)
_resize_main()
return return
if c == curses.KEY_RESIZE: if c == curses.KEY_RESIZE:
# No need to call _resize_main(), because the search window is
# fullscreen.
# We adjust the scroll so that the selected node stays visible in # We adjust the scroll so that the selected node stays visible in
# the list when the terminal is resized, hence the 'scroll' # the list when the terminal is resized, hence the 'scroll'
# assignment # assignment
@ -1433,6 +1434,33 @@ def _jump_to_dialog():
s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, s, s_i, hscroll = _edit_text(c, s, s_i, hscroll,
edit_box.getmaxyx()[1] - 2) edit_box.getmaxyx()[1] - 2)
# Obscure Python: We never pass a value for cached_search_strings, and it keeps
# pointing to the same list. This avoids a global.
def _search_strings(cached_search_strings=[]):
# Returns a list with (node, node_string) tuples for all symbol menu nodes,
# sorted by symbol name.
#
# node_string is a string containing the symbol's name and prompt. It is
# matched against the regex(es) the user inputs during search, and doubles
# as the string displayed for the node in the list of matches.
# This is a static list. Only computing it once makes the search dialog
# come up a bit faster after the first time it's entered.
if not cached_search_strings:
# Defined symbols sorted by name, with duplicates removed.
#
# Duplicates appear when symbols have multiple menu nodes (definition
# locations), but they appear in menu order, which isn't what we want
# here. We'd still need to go through sym.nodes as well.
for sym in sorted(set(_kconf.defined_syms), key=lambda sym: sym.name):
for node in sym.nodes:
node_string = sym.name
if node.prompt:
node_string += ' "{}"'.format(node.prompt[0])
cached_search_strings.append((node, node_string))
return cached_search_strings
def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
sel_node_i, scroll): sel_node_i, scroll):
# Resizes the jump-to dialog to fill the terminal. # Resizes the jump-to dialog to fill the terminal.
@ -1489,24 +1517,16 @@ def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
# bad_re holds the error message from the re.error exception on errors # bad_re holds the error message from the re.error exception on errors
_safe_addstr(matches_win, 0, 0, _safe_addstr(matches_win, 0, 0,
"Bad regular expression: " + bad_re) "Bad regular expression: " + bad_re)
elif not matches: elif not matches:
_safe_addstr(matches_win, 0, 0, "No matches") _safe_addstr(matches_win, 0, 0, "No matches")
else: else:
for i in range(scroll, for i in range(scroll,
min(scroll + matches_win.getmaxyx()[0], len(matches))): min(scroll + matches_win.getmaxyx()[0], len(matches))):
style = _LIST_SEL_STYLE if i == sel_node_i else _LIST_STYLE
sym = matches[i].item _safe_addstr(matches_win, i - scroll, 0, matches[i][1],
_LIST_SEL_STYLE if i == sel_node_i else _LIST_STYLE)
s2 = sym.name
if len(sym.nodes) > 1:
# Give menu locations as well for symbols that are defined in
# multiple locations. The different menu locations will be
# listed next to one another.
s2 += " (in menu {})".format(
_parent_menu(matches[i]).prompt[0])
_safe_addstr(matches_win, i - scroll, 0, s2, style)
matches_win.noutrefresh() matches_win.noutrefresh()
@ -1594,8 +1614,6 @@ def _info_dialog(node):
c = _get_wch_compat(text_win) c = _get_wch_compat(text_win)
if c == curses.KEY_RESIZE: if c == curses.KEY_RESIZE:
# No need to call _resize_main(), because the help window is
# fullscreen
_resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win)
elif c in (curses.KEY_DOWN, "j", "J"): elif c in (curses.KEY_DOWN, "j", "J"):
@ -1622,10 +1640,6 @@ def _info_dialog(node):
"\x1B", # \x1B = ESC "\x1B", # \x1B = ESC
"q", "Q", "h", "H"): "q", "Q", "h", "H"):
# Resize the main display before returning in case the terminal was
# resized while the help dialog was open
_resize_main()
return return
def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win):
@ -2115,7 +2129,7 @@ def _value_str(node):
if item.type == TRISTATE: if item.type == TRISTATE:
if item.assignable == (1, 2): if item.assignable == (1, 2):
return "{{{}}}".format(tri_val_str) # { }/{M}/{*} return "{{{}}}".format(tri_val_str) # {M}/{*}
return "<{}>".format(tri_val_str) return "<{}>".format(tri_val_str)
def _is_y_mode_choice_sym(item): def _is_y_mode_choice_sym(item):