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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: CI

on: [push, pull_request]
on: [push, pull_request, workflow_dispatch]

jobs:
spelling:
Expand Down
3 changes: 2 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ a message as the second (both positionally), and show them in a temporary messag
# Changelog

### v1.4
- Grouped options with no visible children no longer show their header.
- Improved suggestions when trying to bind a key by name, and misspelling it.
- Swap known controller key names between UE3/UE4 versions, based on game.
- Grouped options with no visible children no longer show their header.

### Older
Versions 1.0 through 1.3 were developed as part of the
Expand Down
126 changes: 2 additions & 124 deletions key_matching.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from os import path
from difflib import get_close_matches

from mods_base import Game

Expand Down Expand Up @@ -178,101 +178,6 @@
# endregion
# region Misspellings

# Python has code to suggest other names on an attribute or name error - we want to do the same when
# someone gives an invalid key name.
# Unfortunately, it doesn't seem to be exposed, so we manually replicate it instead
# Based on Python/suggestions.c:calculate_suggestions

# ruff: noqa: ERA001

MOVE_COST = 2
CASE_COST = 1
MAX_STRING_SIZE = 40


def _substitution_cost(a: str, b: str) -> int:
if a == b:
return 0
if a.lower() == b.lower():
return CASE_COST

return MOVE_COST


def _levenshtein_distance(a: str, b: str, max_cost: int) -> int:
"""Calculate the Levenshtein distance between string1 and string2."""

# Both strings are the same
if a == b:
return 0

# Trim away common affixes.
common_prefix = path.commonprefix((a, b))
a = a.removeprefix(common_prefix)
b = b.removeprefix(common_prefix)

common_suffix = path.commonprefix((a[::-1], b[::-1]))
a = a.removesuffix(common_suffix)
b = b.removesuffix(common_suffix)

a_size = len(a)
b_size = len(b)
if a_size == 0 or b_size == 0:
return (a_size + b_size) * MOVE_COST

if a_size > MAX_STRING_SIZE or b_size > MAX_STRING_SIZE:
return max_cost + 1

# Prefer shorter buffer
if b_size < a_size:
a, b = b, a
a_size, b_size = b_size, a_size

# quick fail when a match is impossible.
if (b_size - a_size) * MOVE_COST > max_cost:
return max_cost + 1

# Instead of producing the whole traditional len(a)-by-len(b)
# matrix, we can update just one row in place.
# Initialize the buffer row
# cost from b[:0] to a[:i+1]
buffer = [(i + 1) * MOVE_COST for i in range(a_size)]

result = 0
for b_index in range(b_size):
code = b[b_index]
# cost(b[:b_index], a[:0]) == b_index * MOVE_COST
distance = result = b_index * MOVE_COST
minimum = None
for index in range(a_size):
# cost(b[:b_index+1], a[:index+1]) = min(
# # 1) substitute
# cost(b[:b_index], a[:index])
# + substitution_cost(b[b_index], a[index]),
# # 2) delete from b
# cost(b[:b_index], a[:index+1]) + MOVE_COST,
# # 3) delete from a
# cost(b[:b_index+1], a[index]) + MOVE_COST
# )

# 1) Previous distance in this row is cost(b[:b_index], a[:index])
substitute = distance + _substitution_cost(code, a[index])
# 2) cost(b[:b_index], a[:index+1]) from previous row
distance = buffer[index]
# 3) existing result is cost(b[:b_index+1], a[index])

insert_delete = min(result, distance) + MOVE_COST
result = min(insert_delete, substitute)

# cost(b[:b_index+1], a[:index+1])
buffer[index] = result
if minimum is None or result < minimum:
minimum = result
if minimum is None or minimum > max_cost:
# Everything in this row is too big, so bail early.
return max_cost + 1
return result


def suggest_misspelt_key(invalid_key: str) -> Iterable[str]:
"""
Expand All @@ -283,34 +188,7 @@ def suggest_misspelt_key(invalid_key: str) -> Iterable[str]:
Returns:
A list of possible misspellings (which may be empty).
"""
suggestion_distance: int | None = None
suggestion: str | None = None
for item in KNOWN_KEYS:
if item == invalid_key:
continue

# No more than 1/3 of the involved characters should need changed.
max_distance = (len(invalid_key) + len(item) + 3) * MOVE_COST // 6

# Don't take matches we've already beaten.
if suggestion_distance is not None and (suggestion_distance - 1) < max_distance:
max_distance = suggestion_distance - 1

current_distance = _levenshtein_distance(invalid_key, item, max_distance)

if current_distance > max_distance:
continue
if (
suggestion is None
or suggestion_distance is None
or current_distance < suggestion_distance
):
suggestion = item
suggestion_distance = current_distance

if suggestion is None:
return ()
return (suggestion,)
return get_close_matches(invalid_key, KNOWN_KEYS)


# endregion
Expand Down