From 5edffe63d3b6f503ad29da08f281ea88682f5f90 Mon Sep 17 00:00:00 2001 From: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> Date: Mon, 15 May 2023 17:50:39 +0200 Subject: feat: Make search fuzzy (#342) * feat: Enable fuzzy search for Thunderstore mods * feat: Enable fuzzy search for pull requests * refactor: Move filter into own util file * docs: Add comment explaining function in detail --- src-vue/src/components/PullRequestsSelector.vue | 13 +++++-------- src-vue/src/utils/filter.ts | 26 +++++++++++++++++++++++++ src-vue/src/views/mods/ThunderstoreModsView.vue | 9 ++++++--- 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 src-vue/src/utils/filter.ts diff --git a/src-vue/src/components/PullRequestsSelector.vue b/src-vue/src/components/PullRequestsSelector.vue index 4bc8f9f5..fe103edc 100644 --- a/src-vue/src/components/PullRequestsSelector.vue +++ b/src-vue/src/components/PullRequestsSelector.vue @@ -80,6 +80,7 @@ import { defineComponent } from 'vue' import { PullRequestType } from '../../../src-tauri/bindings/PullRequestType'; import { PullsApiResponseElement } from '../../../src-tauri/bindings/PullsApiResponseElement'; +import { fuzzy_filter } from "../utils/filter"; export default defineComponent({ name: 'PullRequestsSelector', @@ -101,10 +102,8 @@ export default defineComponent({ } return this.pull_requests_launcher.filter(pr => - // Check PR id - pr.number.toString().indexOf(this.launcherSearch) !== -1 - // Check PR title - || pr.title.toLowerCase().indexOf(this.launcherSearch.toLowerCase()) !== -1); + // Check PR id and title + fuzzy_filter(pr.number.toString(), this.launcherSearch) || fuzzy_filter(pr.title, this.launcherSearch)); }, filtered_mods_pull_requests(): PullsApiResponseElement[] { if (this.modsSearch.length === 0) { @@ -112,10 +111,8 @@ export default defineComponent({ } return this.pull_requests_mods.filter(pr => - // Check PR id - pr.number.toString().indexOf(this.modsSearch) !== -1 - // Check PR title - || pr.title.toLowerCase().indexOf(this.modsSearch.toLowerCase()) !== -1); + // Check PR id and title + fuzzy_filter(pr.number.toString(), this.modsSearch) || fuzzy_filter(pr.title, this.modsSearch)); }, }, methods: { diff --git a/src-vue/src/utils/filter.ts b/src-vue/src/utils/filter.ts new file mode 100644 index 00000000..b85b9623 --- /dev/null +++ b/src-vue/src/utils/filter.ts @@ -0,0 +1,26 @@ +/** + * Implements a fuzzy filter + * Iterates through chars of `search_term` and checks if each char exists in consecutive order in `text`. + * For example, this means that `text="Gecko"` and `search_term="geo"` will return `true` + * but using `text="Gecko"` and `search_term="goe"` will return `false` + * + * Implements a subset of "fuzzy string searching" + * https://en.wikipedia.org/wiki/Approximate_string_matching + */ +function fuzzy_filter(text: string, search_term: string): boolean { + const lowercase_text = text.toLowerCase(); + const lowercase_search_term = search_term.toLowerCase(); + + let previousIndex = -1; + for (let i = 0; i < lowercase_search_term.length; i++) { + const char = lowercase_search_term[i]; + const currentIndex = lowercase_text.indexOf(char, previousIndex + 1); + if (currentIndex === -1) { + return false; + } + previousIndex = currentIndex; + } + + return true; +} +export { fuzzy_filter }; diff --git a/src-vue/src/views/mods/ThunderstoreModsView.vue b/src-vue/src/views/mods/ThunderstoreModsView.vue index 410af5f3..58a00367 100644 --- a/src-vue/src/views/mods/ThunderstoreModsView.vue +++ b/src-vue/src/views/mods/ThunderstoreModsView.vue @@ -51,6 +51,7 @@ import ThunderstoreModCard from "../../components/ThunderstoreModCard.vue"; import { ElScrollbar, ScrollbarInstance } from "element-plus"; import { SortOptions } from "../../utils/SortOptions.d"; import { ThunderstoreModVersion } from "../../../../src-tauri/bindings/ThunderstoreModVersion"; +import { fuzzy_filter } from "../../utils/filter"; export default defineComponent({ name: "ThunderstoreModsView", @@ -79,9 +80,11 @@ export default defineComponent({ return this.mods.filter((mod: ThunderstoreMod) => { // Filter with search words (only if search field isn't empty) const inputMatches: boolean = this.searchValue.length === 0 - || (mod.name.toLowerCase().includes(this.searchValue) - || mod.owner.toLowerCase().includes(this.searchValue) - || mod.versions[0].description.toLowerCase().includes(this.searchValue)); + || ( + fuzzy_filter(mod.name, this.searchValue) || + fuzzy_filter(mod.owner, this.searchValue) || + mod.versions[0].description.toLowerCase().includes(this.searchValue) + ); // Filter with categories (only if some categories are selected) const categoriesMatch: boolean = this.selectedCategories.length === 0 -- cgit v1.2.3