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