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