Skip to content
Draft
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
74 changes: 74 additions & 0 deletions scripts/kconfig/config_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3

# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
# SPDX-License-Identifier: Apache-2.0

"""
Common utilities for Kconfig configuration interfaces.

This module provides shared functionality for menuconfig.py and guiconfig.py.
"""

import re

from kconfiglib import Choice, Symbol


def score_search_matches(search_str, nodes):
"""
Scores and sorts search results for Kconfig nodes based on relevance.

This implements a basic scoring system where:
- A match in a symbol's name is given more weight than a match in its prompt
- Field-length normalization is applied so that the shorter the field, the higher its relevance

Args:
search_str: The search string (space-separated regexes)
nodes: List of MenuNode objects to search through

Returns:
List of tuples (node, score) sorted by score (highest first)
"""
# Parse the search string into regexes
try:
regexes = [re.compile(regex.lower()) for regex in search_str.split()]
# If no regexes, all nodes match, order is unchanged
if len(regexes) == 0:
return [(node, 1) for node in nodes]
except re.error:
# Invalid regex - return empty results
return []

NAME_WEIGHT = 2.0
PROMPT_WEIGHT = 1.0

scored_results = []

for node in nodes:
# Get lowercase versions for matching
name = ""
if isinstance(node.item, Symbol | Choice):
name = node.item.name.lower() if node.item.name else ""

prompt = node.prompt[0].lower() if node.prompt else ""

# Check if all regexes match in either name or prompt
name_matches = name and all(regex.search(name) for regex in regexes)
prompt_matches = prompt and all(regex.search(prompt) for regex in regexes)

if not (name_matches or prompt_matches):
continue

# Apply field-length normalization (shorter fields = higher relevance)
score = 0
if name_matches:
score += NAME_WEIGHT / (len(name) ** 0.5)
if prompt_matches:
score += PROMPT_WEIGHT / (len(prompt) ** 0.5)

scored_results.append((node, score))

# Sort by score (highest first)
scored_results.sort(key=lambda x: x[1], reverse=True)

return scored_results
75 changes: 13 additions & 62 deletions scripts/kconfig/guiconfig.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3

Check warning on line 1 in scripts/kconfig/guiconfig.py

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

License may not be allowed

scripts/kconfig/guiconfig.py:1 License file for 'ISC' not found in /LICENSES. Please check https://docs.zephyrproject.org/latest/contribute/guidelines.html#components-using-other-licenses.

# Copyright (c) 2019, Nordic Semiconductor ASA and Ulf Magnusson
# SPDX-License-Identifier: ISC
Expand Down Expand Up @@ -61,23 +61,11 @@
import errno
import os
import re
import sys

_PY2 = sys.version_info[0] < 3

if _PY2:
# Python 2
from Tkinter import *
import ttk
import tkFont as font
import tkFileDialog as filedialog
import tkMessageBox as messagebox
else:
# Python 3
from tkinter import *
import tkinter.ttk as ttk
import tkinter.font as font
from tkinter import filedialog, messagebox

from tkinter import *
import tkinter.ttk as ttk
import tkinter.font as font
from tkinter import filedialog, messagebox

from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \
BOOL, TRISTATE, STRING, INT, HEX, \
Expand All @@ -87,6 +75,8 @@
TRI_TO_STR, TYPE_TO_STR, \
standard_kconfig, standard_config_filename

from config_utils import score_search_matches


# If True, use GIF image data embedded in this file instead of separate GIF
# files. See _load_images().
Expand Down Expand Up @@ -501,7 +491,7 @@
# Panedwindow and the Treeview. This code is shared between the main window
# and the jump-to dialog.

panedwindow = ttk.Panedwindow(parent, orient=VERTICAL)
panedwindow = ttk.Panedwindow(parent, orient="vertical")

tree_frame, tree = _create_kconfig_tree(panedwindow)
desc_frame, desc = _create_kconfig_desc(panedwindow)
Expand Down Expand Up @@ -1124,11 +1114,6 @@
if sc.type in (INT, HEX, STRING):
s = _set_val_dialog(node, parent)

# Tkinter can return 'unicode' strings on Python 2, which Kconfiglib
# can't deal with. UTF-8-encode the string to work around it.
if _PY2 and isinstance(s, unicode):
s = s.encode("utf-8", "ignore")

if s is not None:
_set_val(sc, s)

Expand Down Expand Up @@ -1859,12 +1844,11 @@
_jump_to_tree.selection_set(())

try:
# We could use re.IGNORECASE here instead of lower(), but this is
# faster for regexes like '.*debug$' (though the '.*' is redundant
# there). Those probably have bad interactions with re.search(), which
# matches anywhere in the string.
regex_searches = [re.compile(regex).search
for regex in search_string.lower().split()]
scored_sc_nodes = score_search_matches(search_string, _sorted_sc_nodes())
scored_menu_comment_nodes = score_search_matches(search_string, _sorted_menu_comment_nodes())

_jump_to_matches = [node for node, _ in scored_sc_nodes + scored_menu_comment_nodes]

except re.error as e:
msg = "Bad regular expression"
# re.error.msg was added in Python 3.5
Expand All @@ -1875,39 +1859,6 @@
_jump_to_tree.set_children("")
return

_jump_to_matches = []
add_match = _jump_to_matches.append

for node in _sorted_sc_nodes():
# Symbol/choice
sc = node.item

for search in regex_searches:
# Both the name and the prompt might be missing, since
# we're searching both symbols and choices

# Does the regex match either the symbol name or the
# prompt (if any)?
if not (sc.name and search(sc.name.lower()) or
node.prompt and search(node.prompt[0].lower())):

# Give up on the first regex that doesn't match, to
# speed things up a bit when multiple regexes are
# entered
break

else:
add_match(node)

# Search menus and comments

for node in _sorted_menu_comment_nodes():
for search in regex_searches:
if not search(node.prompt[0].lower()):
break
else:
add_match(node)

msglabel["text"] = "" if _jump_to_matches else "No matches"

_update_jump_to_display()
Expand Down
52 changes: 6 additions & 46 deletions scripts/kconfig/menuconfig.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3

Check warning on line 1 in scripts/kconfig/menuconfig.py

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

License may not be allowed

scripts/kconfig/menuconfig.py:1 License file for 'ISC' not found in /LICENSES. Please check https://docs.zephyrproject.org/latest/contribute/guidelines.html#components-using-other-licenses.

# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson
# SPDX-License-Identifier: ISC
Expand Down Expand Up @@ -223,6 +223,8 @@
TRI_TO_STR, TYPE_TO_STR, \
standard_kconfig, standard_config_filename

from config_utils import score_search_matches


#
# Configuration variables
Expand Down Expand Up @@ -1144,7 +1146,6 @@
global _cur_menu
global _shown
global _sel_node_i
global _menu_scroll
global _show_all
global _parent_screen_rows

Expand Down Expand Up @@ -2078,54 +2079,13 @@
prev_s = s

try:
# We could use re.IGNORECASE here instead of lower(), but this
# is noticeably less jerky while inputting regexes like
# '.*debug$' (though the '.*' is redundant there). Those
# probably have bad interactions with re.search(), which
# matches anywhere in the string.
#
# It's not horrible either way. Just a bit smoother.
regex_searches = [re.compile(regex).search
for regex in s.lower().split()]

# No exception thrown, so the regexes are okay
# Use the scoring function for symbols and choices
bad_re = None

# List of matching nodes
matches = []
add_match = matches.append

# Search symbols and choices

for node in _sorted_sc_nodes():
# Symbol/choice
sc = node.item

for search in regex_searches:
# Both the name and the prompt might be missing, since
# we're searching both symbols and choices

# Does the regex match either the symbol name or the
# prompt (if any)?
if not (sc.name and search(sc.name.lower()) or
node.prompt and search(node.prompt[0].lower())):

# Give up on the first regex that doesn't match, to
# speed things up a bit when multiple regexes are
# entered
break

else:
add_match(node)

# Search menus and comments
scored_sc_nodes = score_search_matches(s, _sorted_sc_nodes())
scored_menu_comment_nodes = score_search_matches(s, _sorted_menu_comment_nodes())

for node in _sorted_menu_comment_nodes():
for search in regex_searches:
if not search(node.prompt[0].lower()):
break
else:
add_match(node)
matches = [node for node, _ in scored_sc_nodes + scored_menu_comment_nodes]

except re.error as e:
# Bad regex. Remember the error message so we can show it.
Expand Down
Loading