diff --git a/src/TSMapEditor/Helpers.cs b/src/TSMapEditor/Helpers.cs index 670423175..f18512b18 100644 --- a/src/TSMapEditor/Helpers.cs +++ b/src/TSMapEditor/Helpers.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using FuzzySharp; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; @@ -739,16 +740,115 @@ public static bool IsCloningSupported(IMovable objectToClone) return false; } - public static IniFile ReadConfigINI(string path) + /// + /// Calculates a fuzzy search score between a search string and a target string. + /// The score is based on similarity and optionally checks individual parts of the target string. + /// Significantly improves score of exact matches and partial matches of the search string. + /// + /// The string to search for. + /// The string to compare against. + /// Whether to check multiple parts separated by a space. Useful for texts that has ININame or ID, followed by the object name. + /// An integer score representing the similarity between the strings. + public static int CalculateFuzzySearchScore(string searchString, string targetString, bool checkParts) { - string customPath = Path.Combine(Environment.CurrentDirectory, "Config", path); - string defaultPath = Path.Combine(Environment.CurrentDirectory, "Config", "Default", path); + targetString = targetString.ToLowerInvariant(); - if (File.Exists(customPath)) - return new IniFile(customPath); + if (string.IsNullOrWhiteSpace(searchString)) + return 100; - return new IniFile(defaultPath); - } + // Fuzzy Search method, used to calculate initial score + int score = Fuzz.Ratio(searchString, targetString); + + List nameParts = []; + + if (checkParts) + { + int spaceIndex = targetString.IndexOf(" "); + if (spaceIndex >= 0) + { + string firstPart = targetString.Substring(0, spaceIndex); + string secondPart = targetString.Substring(spaceIndex + 1); + nameParts.Add(firstPart); + nameParts.Add(secondPart); + } + else + { + nameParts.Add(targetString); + } + } + else + { + nameParts.Add(targetString); + } + + foreach (var namePart in nameParts) + { + if (namePart == searchString) + { + score += 100; + break; + } + else if (namePart.StartsWith(searchString)) + { + score += 75; + break; + } + else if (namePart.Contains(searchString)) + { + score += 35; + break; + } + } + + return score; + } + + /// + /// Performs a fuzzy search on a list of items and returns a list of results with their scores. + /// Items are filtered by a minimum score and optionally checked for partial matches. + /// + /// The type of items in the list. + /// The string to search for. + /// The list of items to search through. + /// A function to extract the name or relevant representation of an item. + /// The minimum score required for an item to be included in the results. + /// Whether to check multiple parts separated by a space. Useful for texts that has ININame or ID, followed by the object name. + /// A list of fuzzy search results, each containing an item and a score. + public static List> FuzzySearch(string searchString, List itemsList, Func extractTextFromItem, int minimumScore, bool checkParts) + { + searchString = searchString.ToLowerInvariant(); + + if (string.IsNullOrWhiteSpace(searchString)) + return itemsList.Select(item => new FuzzySearchItem(item, 100)).ToList(); + + var results = itemsList + .Select(item => + { + string itemText = extractTextFromItem(item); + + if (itemText == Constants.NoneValue1 || itemText == Constants.NoneValue2) + return new FuzzySearchItem(item, 0); + + int score = CalculateFuzzySearchScore(searchString, itemText, checkParts); + return new FuzzySearchItem(item, score); + }) + .Where(item => item.Score >= minimumScore) + .OrderByDescending(item => item.Score) + .ToList(); + + return results; + } + + public static IniFile ReadConfigINI(string path) + { + string customPath = Path.Combine(Environment.CurrentDirectory, "Config", path); + string defaultPath = Path.Combine(Environment.CurrentDirectory, "Config", "Default", path); + + if (File.Exists(customPath)) + return new IniFile(customPath); + + return new IniFile(defaultPath); + } public static IniFileEx ReadConfigINIEx(string path, CCFileManager fileManager) { diff --git a/src/TSMapEditor/Models/FuzzySearchItem.cs b/src/TSMapEditor/Models/FuzzySearchItem.cs new file mode 100644 index 000000000..89a661e8b --- /dev/null +++ b/src/TSMapEditor/Models/FuzzySearchItem.cs @@ -0,0 +1,8 @@ +namespace TSMapEditor.Models +{ + public class FuzzySearchItem(T item, int score) + { + public T Item { get; set; } = item; + public int Score { get; set; } = score; + }; +} diff --git a/src/TSMapEditor/TSMapEditor.csproj b/src/TSMapEditor/TSMapEditor.csproj index 19b656cf5..27e081d09 100644 --- a/src/TSMapEditor/TSMapEditor.csproj +++ b/src/TSMapEditor/TSMapEditor.csproj @@ -477,6 +477,7 @@ + diff --git a/src/TSMapEditor/UI/Windows/ScriptsWindow.cs b/src/TSMapEditor/UI/Windows/ScriptsWindow.cs index 028b02e86..a4eca523f 100644 --- a/src/TSMapEditor/UI/Windows/ScriptsWindow.cs +++ b/src/TSMapEditor/UI/Windows/ScriptsWindow.cs @@ -43,6 +43,7 @@ public ScriptsWindow(WindowManager windowManager, Map map, EditorState editorSta private readonly EditorState editorState; private readonly INotificationManager notificationManager; private SelectCellCursorAction selectCellCursorAction; + private readonly int minimumFuzzySearchScore = 50; private EditorListBox lbScriptTypes; private EditorSuggestionTextBox tbFilter; @@ -768,27 +769,34 @@ private void ListScripts() IEnumerable