33 *
44 * This program is free software: you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
6-
76 * the Free Software Foundation, either version 3 of the License, or
87 * (at your option) any later version.
98 *
@@ -32,13 +31,14 @@ import com.lambda.module.Module
3231import com.lambda.module.ModuleRegistry
3332import com.lambda.util.KeyCode
3433import com.lambda.util.StringUtils.capitalize
35- import com.lambda.util.StringUtils.findSimilarStrings
34+ import com.lambda.util.StringUtils.levenshteinDistance
3635import imgui.ImGui
3736import imgui.flag.ImGuiInputTextFlags
3837import imgui.flag.ImGuiStyleVar
3938import imgui.flag.ImGuiWindowFlags
4039import imgui.type.ImString
4140import net.minecraft.client.gui.screen.ChatScreen
41+ import kotlin.math.max
4242
4343object QuickSearch {
4444 private val searchInput = ImString(256)
@@ -50,7 +50,6 @@ object QuickSearch {
5050
5151 private const val DOUBLE_SHIFT_WINDOW_MS = 500L
5252 private const val MAX_RESULTS = 50
53- private const val SIMILARITY_THRESHOLD = 3
5453 private const val WINDOW_FLAGS = ImGuiWindowFlags.AlwaysAutoResize or
5554 ImGuiWindowFlags.NoTitleBar or
5655 ImGuiWindowFlags.NoMove or
@@ -74,22 +73,6 @@ object QuickSearch {
7473 override fun ImGuiBuilder.buildLayout() {
7574 with(ModuleEntry(module)) { buildLayout() }
7675 }
77-
78- companion object {
79- fun search(query: String): List<ModuleResult> {
80- val modules = ModuleRegistry.modules
81- val direct = modules.filter {
82- it.name.lowercase().let { name -> name.startsWith(query) || name.contains(query) }
83- }
84-
85- if (direct.isNotEmpty()) return direct.map(::ModuleResult)
86-
87- val names = modules.map { it.name }.toSet()
88- val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD)
89- return similar.mapNotNull { name -> modules.find { it.name == name } }
90- .map(::ModuleResult)
91- }
92- }
9376 }
9477
9578 private class CommandResult(val command: LambdaCommand) : SearchResult {
@@ -103,23 +86,6 @@ object QuickSearch {
10386 textDisabled(command.description)
10487 }
10588 }
106-
107- companion object {
108- fun search(query: String): List<CommandResult> {
109- val commands = CommandRegistry.commands
110- val direct = commands.filter {
111- val name = it.name.lowercase()
112- name.startsWith(query) || name.contains(query) || it.aliases.any { alias -> alias.lowercase().contains(query) }
113- }
114-
115- if (direct.isNotEmpty()) return direct.map(::CommandResult)
116-
117- val names = commands.map { it.name }.toSet()
118- val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD)
119- return similar.mapNotNull { name -> commands.find { it.name == name } }
120- .map(::CommandResult)
121- }
122- }
12389 }
12490
12591 private class SettingResult(val setting: AbstractSetting<*>, val configurable: Configurable) : SearchResult {
@@ -128,20 +94,6 @@ object QuickSearch {
12894 override fun ImGuiBuilder.buildLayout() {
12995 with(setting) { buildLayout() }
13096 }
131-
132- companion object {
133- fun search(query: String) =
134- Configuration.configurations.flatMap { config ->
135- config.configurables.flatMap { configurable ->
136- val confNameL = configurable.name.lowercase()
137- configurable.settings.filter { setting ->
138- setting.visibility() && (setting.name.lowercase().contains(query))
139- }.map { setting ->
140- SettingResult(setting, configurable)
141- }
142- }
143- }
144- }
14597 }
14698
14799 fun open() {
@@ -182,13 +134,13 @@ object QuickSearch {
182134 return@popupModal
183135 }
184136
185- // val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) &&
186- // !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow)
187- // if (bgClick) {
188- // close()
189- // ImGui.closeCurrentPopup()
190- // return@popupModal
191- // }
137+ // val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) &&
138+ // !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow)
139+ // if (bgClick) {
140+ // close()
141+ // ImGui.closeCurrentPopup()
142+ // return@popupModal
143+ // }
192144
193145 if (shouldFocus) {
194146 ImGui.setKeyboardFocusHere()
@@ -209,7 +161,7 @@ object QuickSearch {
209161 val query = searchInput.get().trim()
210162 if (query.isEmpty()) return@popupModal
211163
212- val results = performSearch(query)
164+ val results = SearchService. performSearch(query)
213165 if (results.isEmpty()) {
214166 textDisabled("Nothing found.")
215167 return@popupModal
@@ -235,10 +187,106 @@ object QuickSearch {
235187 }
236188 }
237189
190+ private object SearchService {
191+ private data class RankedSearchResult(val result: SearchResult, val score: Int)
192+
193+ private const val MODULE_PRIORITY_BONUS = 300
194+ private const val COMMAND_PRIORITY_BONUS = 200
195+
196+ /**
197+ * Calculates a relevance score for a query against a target string.
198+ * Returns 0 for no match. Higher scores are better.
199+ * The `lenient` flag adjusts the threshold for fuzzy matching.
200+ */
201+ private fun calculateScore(query: String, target: String, lenient: Boolean = false): Int {
202+ if (query.isEmpty() || target.isEmpty()) return 0
203+
204+ // 1. Strong Matches (Exact, Prefix, Substring)
205+ if (target == query) return 200
206+ if (target.startsWith(query)) {
207+ val completeness = (query.length * 50) / target.length
208+ return 100 + completeness // Score: 101 - 150
209+ }
210+ if (target.contains(query)) {
211+ val completeness = (query.length * 40) / target.length
212+ return 50 + completeness // Score: 51 - 90
213+ }
238214
239- private fun performSearch(query: String) =
240- listOf(ModuleResult::search, CommandResult::search, SettingResult::search)
241- .flatMap { it(query.lowercase()) }.take(MAX_RESULTS)
215+ // 2. Weak Match (Fuzzy)
216+ val distance = query.levenshteinDistance(target)
217+ val strictThreshold = (query.length / 3).coerceAtLeast(1).coerceAtMost(4)
218+ val lenientThreshold = (query.length / 2).coerceAtLeast(2).coerceAtMost(6)
219+ val threshold = if (lenient) lenientThreshold else strictThreshold
220+
221+ return if (distance <= threshold) {
222+ (50 - (distance * 10)).coerceAtLeast(1) // Score: 1-40
223+ } else {
224+ 0
225+ }
226+ }
227+
228+ /**
229+ * Performs a search and returns a list of ranked results. This is the internal
230+ * implementation that can be run in strict or lenient mode.
231+ */
232+ private fun searchInternal(query: String, lenient: Boolean): List<RankedSearchResult> {
233+ val lowerCaseQuery = query.lowercase()
234+
235+ val moduleResults = ModuleRegistry.modules.mapNotNull { module ->
236+ val nameScore = calculateScore(lowerCaseQuery, module.name.lowercase(), lenient)
237+ val tagScore = calculateScore(lowerCaseQuery, module.tag.name.lowercase(), lenient)
238+ val bestScore = max(nameScore, tagScore)
239+
240+ if (bestScore > 0) {
241+ RankedSearchResult(ModuleResult(module), bestScore + MODULE_PRIORITY_BONUS)
242+ } else null
243+ }
244+
245+ val commandResults = CommandRegistry.commands.mapNotNull { command ->
246+ val nameScore = calculateScore(lowerCaseQuery, command.name.lowercase(), lenient)
247+ val aliasScore = command.aliases.maxOfOrNull { calculateScore(lowerCaseQuery, it.lowercase(), lenient) } ?: 0
248+ val bestScore = max(nameScore, aliasScore)
249+
250+ if (bestScore > 0) {
251+ RankedSearchResult(CommandResult(command), bestScore + COMMAND_PRIORITY_BONUS)
252+ } else null
253+ }
254+
255+ val settingResults = Configuration.configurations.flatMap {
256+ it.configurables.flatMap { configurable ->
257+ configurable.settings
258+ .filter { setting -> setting.visibility() }
259+ .mapNotNull { setting ->
260+ val score = calculateScore(lowerCaseQuery, setting.name.lowercase(), lenient)
261+ if (score > 0) RankedSearchResult(SettingResult(setting, configurable), score) else null
262+ }
263+ }
264+ }
265+
266+ return moduleResults + commandResults + settingResults
267+ }
268+
269+ /**
270+ * Main search entry point. It first attempts a strict search. If no results
271+ * are found, it falls back to a more lenient fuzzy search.
272+ */
273+ fun performSearch(query: String): List<SearchResult> {
274+ // First pass: strict search for high-quality matches.
275+ val strictResults = searchInternal(query, lenient = false)
276+ if (strictResults.isNotEmpty()) {
277+ return strictResults
278+ .sortedByDescending { it.score }
279+ .map { it.result }
280+ .take(MAX_RESULTS)
281+ }
282+
283+ // Second pass: if nothing was found, perform a more generous fuzzy search.
284+ return searchInternal(query, lenient = true)
285+ .sortedByDescending { it.score }
286+ .map { it.result }
287+ .take(MAX_RESULTS)
288+ }
289+ }
242290
243291 private fun buildSettingBreadcrumb(configurableName: String, setting: AbstractSetting<*>): String {
244292 val group = setting.groups
0 commit comments