diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 8c1a5b6ae2..3b1deb394e 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -54,21 +54,19 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) /// The search term. Case insensitive. public List PerformSearch(CKAN.KSP ksp, string term) { + // Remove spaces and special characters from the search term. + term = CkanModule.nonAlphaNums.Replace(term, ""); + var registry = RegistryManager.Instance(ksp).registry; return registry .Available(ksp.VersionCriteria()) .Where((module) => { - // Extract the description. This is an optional field and may be null. - string modDesc = string.Empty; - - if (!string.IsNullOrEmpty(module.description)) - { - modDesc = module.description; - } - // Look for a match in each string. - return module.name.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 || module.identifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 || modDesc.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1; + return module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1; }).ToList(); } diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 5882e0b9b6..51f9002523 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -48,18 +48,17 @@ public ModListScreen(KSPManager mgr, bool dbg) }, 1, 0, ListSortDirection.Descending, (CkanModule m, string filter) => { + // Search for author if (filter.StartsWith("@")) { string authorFilt = filter.Substring(1); if (string.IsNullOrEmpty(authorFilt)) { return true; - } else if (m.author != null) { - foreach (string auth in m.author) { - if (auth.IndexOf(authorFilt, StringComparison.CurrentCultureIgnoreCase) == 0) { - return true; - } - } + } else { + // Remove special characters from search term + authorFilt = CkanModule.nonAlphaNums.Replace(authorFilt, ""); + return m.SearchableAuthors.IndexOf(authorFilt, StringComparison.CurrentCultureIgnoreCase) == 0; } - return false; + // Other special search params: installed, updatable, new, conflicting and dependends } else if (filter.StartsWith("~")) { if (filter.Length <= 1) { // Don't blank the list for just "~" by itself @@ -97,9 +96,12 @@ public ModListScreen(KSPManager mgr, bool dbg) } return false; } else { - return m.identifier.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase) >= 0 - || m.name.IndexOf( filter, StringComparison.CurrentCultureIgnoreCase) >= 0 - || m.@abstract.IndexOf( filter, StringComparison.CurrentCultureIgnoreCase) >= 0; + filter = CkanModule.nonAlphaNums.Replace(filter, ""); + + return m.SearchableIdentifier.IndexOf( filter, StringComparison.CurrentCultureIgnoreCase) >= 0 + || m.SearchableName.IndexOf( filter, StringComparison.CurrentCultureIgnoreCase) >= 0 + || m.SearchableAbstract.IndexOf( filter, StringComparison.CurrentCultureIgnoreCase) >= 0 + || m.SearchableDescription.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase) >= 0; } } ); diff --git a/Core/Registry/AvailableModule.cs b/Core/Registry/AvailableModule.cs index 6684e6f6e7..b3910c0c08 100644 --- a/Core/Registry/AvailableModule.cs +++ b/Core/Registry/AvailableModule.cs @@ -28,8 +28,9 @@ private AvailableModule() } [OnDeserialized] - internal void SetIdentifier(StreamingContext context) + internal void DeserialisationFixes(StreamingContext context) { + // Set identifier var mod = module_version.Values.LastOrDefault(); identifier = mod.identifier; Debug.Assert(module_version.Values.All(m=>identifier.Equals(m.identifier))); diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 3071cca452..24f6f24ec7 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -480,6 +480,21 @@ public List ProvidesList } } + // These are used to simplify the search by dropping special chars. + [JsonIgnore] + public string SearchableName; + [JsonIgnore] + public string SearchableIdentifier; + [JsonIgnore] + public string SearchableAbstract; + [JsonIgnore] + public string SearchableDescription; + [JsonIgnore] + public string SearchableAuthors; + // This regex finds all those special chars. + [JsonIgnore] + public static readonly Regex nonAlphaNums = new Regex("[^a-zA-Z0-9]", RegexOptions.Compiled); + #endregion #region Constructors @@ -527,7 +542,6 @@ public CkanModule(string json, IGameComparator comparator) // Check everything in the spec is defined. // TODO: This *can* and *should* be done with JSON attributes! - foreach (string field in required_fields) { object value = null; @@ -552,6 +566,21 @@ public CkanModule(string json, IGameComparator comparator) throw new BadMetadataKraken(null, error); } } + + // Calculate the Searchables. + CalculateSearchables(); + } + + /// + /// Calculate the mod properties used for searching via Regex. + /// + public void CalculateSearchables() + { + SearchableIdentifier = identifier == null ? string.Empty : CkanModule.nonAlphaNums.Replace(identifier, ""); + SearchableName = name == null ? string.Empty : CkanModule.nonAlphaNums.Replace(name, ""); + SearchableAbstract = @abstract == null ? string.Empty : CkanModule.nonAlphaNums.Replace(@abstract, ""); + SearchableDescription = description == null ? string.Empty : CkanModule.nonAlphaNums.Replace(description, ""); + SearchableAuthors = author == null ? string.Empty : CkanModule.nonAlphaNums.Replace(String.Join("", author), ""); } public string serialise() @@ -575,6 +604,8 @@ private void DeSerialisationFixes(StreamingContext like_i_could_care) license = license ?? new List { License.UnknownLicense }; @abstract = @abstract ?? string.Empty; name = name ?? string.Empty; + + CalculateSearchables(); } /// diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index e82dcf4976..b1ca27d5cf 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -248,7 +248,7 @@ Component - Form + Form SelectionDialog.cs diff --git a/GUI/GUIMod.cs b/GUI/GUIMod.cs index 60a21362b3..ff3d3f9705 100644 --- a/GUI/GUIMod.cs +++ b/GUI/GUIMod.cs @@ -3,7 +3,6 @@ using System.Windows.Forms; using System.Linq; using CKAN.Versioning; -using CKAN.GameVersionProviders; namespace CKAN { @@ -32,6 +31,7 @@ public sealed class GUIMod public string KSPCompatibilityLong { get; private set; } public string Abstract { get; private set; } + public string Description { get; private set; } public string Homepage { get; private set; } public string Identifier { get; private set; } public bool IsInstallChecked { get; set; } @@ -41,6 +41,12 @@ public sealed class GUIMod public bool IsCKAN { get; private set; } public string Abbrevation { get; private set; } + public string SearchableName { get; private set; } + public string SearchableIdentifier { get; private set; } + public string SearchableAbstract { get; private set; } + public string SearchableDescription { get; private set; } + public string SearchableAuthors { get; private set; } + /// /// Return whether this mod is installable. /// Used for determining whether to show a checkbox in the leftmost column. @@ -99,6 +105,7 @@ public GUIMod(CkanModule mod, IRegistryQuerier registry, KspVersionCriteria curr Name = mod.name.Trim(); Abstract = mod.@abstract.Trim(); + Description = mod.description?.Trim() ?? string.Empty; Abbrevation = new string(Name.Split(' ').Where(s => s.Length > 0).Select(s => s[0]).ToArray()); Authors = mod.author == null ? "N/A" : String.Join(",", mod.author); @@ -116,6 +123,12 @@ public GUIMod(CkanModule mod, IRegistryQuerier registry, KspVersionCriteria curr ?? "N/A"; } + // Get the Searchables. + SearchableName = mod.SearchableName; + SearchableAbstract = mod.SearchableAbstract; + SearchableDescription = mod.SearchableDescription; + SearchableAuthors = mod.SearchableAuthors; + UpdateIsCached(); } @@ -186,8 +199,9 @@ public GUIMod(string identifier, IRegistryQuerier registry, KspVersionCriteria c } // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. - Homepage = "N/A"; + + SearchableIdentifier = CkanModule.nonAlphaNums.Replace(Identifier, ""); } /// diff --git a/GUI/MainModInfo.cs b/GUI/MainModInfo.cs index 48bc5095ec..5c8133c506 100644 --- a/GUI/MainModInfo.cs +++ b/GUI/MainModInfo.cs @@ -135,9 +135,9 @@ private void UpdateModInfo(GUIMod gui_module) Util.Invoke(MetadataModuleVersionTextBox, () => MetadataModuleVersionTextBox.Text = gui_module.LatestVersion.ToString()); Util.Invoke(MetadataModuleLicenseTextBox, () => MetadataModuleLicenseTextBox.Text = string.Join(", ", module.license)); Util.Invoke(MetadataModuleAuthorTextBox, () => MetadataModuleAuthorTextBox.Text = gui_module.Authors); - Util.Invoke(MetadataModuleAbstractLabel, () => MetadataModuleAbstractLabel.Text = module.@abstract); - Util.Invoke(MetadataModuleDescriptionTextBox, () => MetadataModuleDescriptionTextBox.Text = module.description); - Util.Invoke(MetadataIdentifierTextBox, () => MetadataIdentifierTextBox.Text = module.identifier); + Util.Invoke(MetadataModuleAbstractLabel, () => MetadataModuleAbstractLabel.Text = gui_module.Abstract); + Util.Invoke(MetadataModuleDescriptionTextBox, () => MetadataModuleDescriptionTextBox.Text = gui_module.Description); + Util.Invoke(MetadataIdentifierTextBox, () => MetadataIdentifierTextBox.Text = gui_module.Identifier); // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. Util.Invoke(MetadataModuleHomePageLinkLabel, () => MetadataModuleHomePageLinkLabel.Text = gui_module.Homepage.ToString()); diff --git a/GUI/MainModList.cs b/GUI/MainModList.cs index 2e90a74516..43bcf90780 100644 --- a/GUI/MainModList.cs +++ b/GUI/MainModList.cs @@ -802,7 +802,7 @@ public async Task> ComputeChangeSetFromModList( public bool IsVisible(GUIMod mod) { var nameMatchesFilter = IsNameInNameFilter(mod); - var authorMatchesFilter = IsAuthorInauthorFilter(mod); + var authorMatchesFilter = IsAuthorInAuthorFilter(mod); var abstractMatchesFilter = IsAbstractInDescriptionFilter(mod); var modMatchesType = IsModInFilter(ModFilter, mod); var isVisible = nameMatchesFilter && modMatchesType && authorMatchesFilter && abstractMatchesFilter; @@ -934,19 +934,26 @@ public string StripEpoch(string version) private bool IsNameInNameFilter(GUIMod mod) { - return mod.Name.IndexOf(ModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1 - || mod.Abbrevation.IndexOf(ModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1 - || mod.Identifier.IndexOf(ModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1; + string sanitisedModNameFilter = CkanModule.nonAlphaNums.Replace(ModNameFilter, ""); + + return mod.Abbrevation.IndexOf(ModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1 + || mod.SearchableName.IndexOf(sanitisedModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1 + || mod.SearchableIdentifier.IndexOf(sanitisedModNameFilter, StringComparison.InvariantCultureIgnoreCase) != -1; } - private bool IsAuthorInauthorFilter(GUIMod mod) + private bool IsAuthorInAuthorFilter(GUIMod mod) { - return mod.Authors.IndexOf(ModAuthorFilter, StringComparison.InvariantCultureIgnoreCase) != -1; + string sanitisedModAuthorFilter = CkanModule.nonAlphaNums.Replace(ModAuthorFilter, ""); + + return mod.SearchableAuthors.IndexOf(sanitisedModAuthorFilter, StringComparison.InvariantCultureIgnoreCase) != -1; } private bool IsAbstractInDescriptionFilter(GUIMod mod) { - return mod.Abstract.IndexOf(ModDescriptionFilter, StringComparison.InvariantCultureIgnoreCase) != -1; + string sanitisedModDescriptionFilter = CkanModule.nonAlphaNums.Replace(ModDescriptionFilter, ""); + + return mod.SearchableAbstract.IndexOf(sanitisedModDescriptionFilter, StringComparison.InvariantCultureIgnoreCase) != -1 + || mod.SearchableDescription.IndexOf(sanitisedModDescriptionFilter, StringComparison.InvariantCultureIgnoreCase) != -1; } private static bool IsModInFilter(GUIModFilter filter, GUIMod m) diff --git a/GUI/SelectionDialog.cs b/GUI/SelectionDialog.cs index cb04ff19ed..2123aaabb6 100644 --- a/GUI/SelectionDialog.cs +++ b/GUI/SelectionDialog.cs @@ -3,7 +3,7 @@ namespace CKAN { - public partial class SelectionDialog : FormCompatibility + public partial class SelectionDialog : Form { int currentSelected;