diff --git a/.gitignore b/.gitignore
index c61bfd55..f7252b73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ libs/
!**/src/main/**/build/
!**/src/test/**/build/
.aider*
+.junie/
### IntelliJ IDEA ###
.intellijPlatform
diff --git a/build.gradle.kts b/build.gradle.kts
index 3db9a490..d4943384 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -27,7 +27,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
// Use the same version and group for the jar and the plugin
-val currentVersion = "2.3.0"
+val currentVersion = "2.4.0"
val myGroup = "com.mituuz"
version = currentVersion
group = myGroup
@@ -41,16 +41,15 @@ intellijPlatform {
Version $currentVersion
-
- Update dependencies
-
- -
- Fix ETD errors
-
- -
- Update minimum version to 2026.1
-
- -
- Add fallback for finding rg with wsl2
+ Add file recency scoring
+
+ -
+ LRU cache for file paths
+
+ -
+ Scoring is based on the recency of the file access and the frequency of the file access
+
+
""".trimIndent()
diff --git a/changelog.md b/changelog.md
index 8b8e1c15..8f4472f7 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
# Changelog
+## Version 2.4.0
+
+- Add file recency scoring
+ - LRU cache for file paths
+ - Scoring is based on the recency of the file access and the frequency of the file access
+
## Version 2.3.0
- Update dependencies
diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt
index cce75ecb..bb95e4ae 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt
@@ -31,6 +31,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.mituuz.fuzzier.actions.FuzzyAction
import com.mituuz.fuzzier.entities.*
import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector
+import com.mituuz.fuzzier.settings.FuzzierSettingsService
import com.mituuz.fuzzier.util.FuzzierUtil
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
@@ -64,12 +65,19 @@ abstract class FilesystemAction : FuzzyAction() {
addAll(projectState.exclusionSet)
addAll(globalState.globalExclusionSet)
}
+ val fileUsageStats = getFileUsageMap(projectState)
return StringEvaluator(
combinedExclusions,
projectState.modules,
+ fileUsageStats
)
}
+ fun getFileUsageMap(state: FuzzierSettingsService.State): Map =
+ state.recentFiles.filter { it.filePath.isNotBlank() }.withIndex().associate { (recentIndex, stats) ->
+ stats.filePath to FileUsageStats(recentIndex, stats.accessCount)
+ }
+
/**
* Processes a set of IterationFiles concurrently
* @return a priority list which has been size limited and sorted
@@ -100,7 +108,9 @@ abstract class FilesystemAction : FuzzyAction() {
globalState.matchWeightSingleChar,
globalState.matchWeightStreakModifier,
globalState.matchWeightPartialPath,
- globalState.matchWeightFilename
+ globalState.matchWeightFilename,
+ globalState.matchWeightFrequency,
+ globalState.matchWeightRecency
)
coroutineScope {
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
index 593355ff..317233ff 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
@@ -311,6 +311,24 @@ class FuzzierGlobalSettingsComponent(
false
)
+ val matchWeightFrequency = SettingsComponent(
+ JBIntSpinner(10, 0, 100), "Match weight: Frequency boost",
+ """
+ How much score should a frequency boost give.
+ Frequency boost is based on how many times a file has been accessed.
+ """.trimIndent(),
+ false
+ )
+
+ val matchWeightRecency = SettingsComponent(
+ JBIntSpinner(10, 0, 100), "Match weight: Recency boost",
+ """
+ How much score should a recency boost give.
+ Recency boost is based on how recently a file has been accessed.
+ """.trimIndent(),
+ false
+ )
+
/////////////////////////////////////////////////////////////////
// Test bench
/////////////////////////////////////////////////////////////////
@@ -362,6 +380,8 @@ class FuzzierGlobalSettingsComponent(
.addComponent(matchWeightPartialPath)
.addComponent(matchWeightStreakModifier)
.addComponent(matchWeightFilename)
+ .addComponent(matchWeightFrequency)
+ .addComponent(matchWeightRecency)
.addSeparator()
.addComponent(JBLabel("Test bench
"))
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
index ac871b04..a2a3afa9 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
@@ -53,7 +53,7 @@ import javax.swing.table.DefaultTableModel
class TestBenchComponent : JPanel(), Disposable {
private val columnNames =
- arrayOf("Filename", "Filepath", "Streak", "MultiMatch", "PartialPath", "Filename", "Total")
+ arrayOf("Filename", "Filepath", "Streak", "MultiMatch", "PartialPath", "Filename", "Freq", "Recency", "Total")
private val table = JBTable()
private var searchField = EditorTextField()
private var debounceJob: Job? = null
@@ -145,13 +145,18 @@ class TestBenchComponent : JPanel(), Disposable {
addAll(liveGlobalExclusions)
}
+ val fileUsageMap =
+ projectState.recentFiles.filter { it.filePath.isNotBlank() }.withIndex().associate { (recentIndex, stats) ->
+ stats.filePath to FileUsageStats(recentIndex, stats.accessCount)
+ }
+
currentUpdateListContentJob?.cancel()
currentUpdateListContentJob = actionScope.launch {
table.setPaintBusy(true)
try {
val stringEvaluator = StringEvaluator(
- combinedExclusions, project.service().state.modules
+ combinedExclusions, project.service().state.modules, fileUsageMap
)
val iterationEntries = withContext(Dispatchers.Default) {
@@ -169,9 +174,8 @@ class TestBenchComponent : JPanel(), Disposable {
)
}
- val sortedList =
- listModel.elements().toList()
- .sortedByDescending { (it as FuzzyMatchContainer).getScore(prioritizeShorterDirPaths) }
+ val sortedList = listModel.elements().toList()
+ .sortedByDescending { (it as FuzzyMatchContainer).getScore(prioritizeShorterDirPaths) }
val data: Array> = sortedList.map {
arrayOf(
(it as FuzzyMatchContainer).filename as Any,
@@ -180,6 +184,8 @@ class TestBenchComponent : JPanel(), Disposable {
it.score.multiMatchScore as Any,
it.score.partialPathScore as Any,
it.score.filenameScore as Any,
+ it.score.frequencyScore as Any,
+ it.score.recencyScore as Any,
it.score.getTotalScore() as Any
)
}.toTypedArray()
@@ -234,8 +240,7 @@ class TestBenchComponent : JPanel(), Disposable {
val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
val processedFiles = ConcurrentHashMap.newKeySet()
val priorityQueue = PriorityQueue(
- fileListLimit + 1,
- compareBy { it.getScore(prioritizeShorterDirPaths) })
+ fileListLimit + 1, compareBy { it.getScore(prioritizeShorterDirPaths) })
val queueLock = Any()
var minimumScore: Int? = null
@@ -255,7 +260,9 @@ class TestBenchComponent : JPanel(), Disposable {
liveSettingsComponent.matchWeightSingleChar.getIntSpinner().value as Int,
liveSettingsComponent.matchWeightStreakModifier.getIntSpinner().value as Int,
liveSettingsComponent.matchWeightPartialPath.getIntSpinner().value as Int,
- liveSettingsComponent.matchWeightFilename.getIntSpinner().value as Int
+ liveSettingsComponent.matchWeightFilename.getIntSpinner().value as Int,
+ liveSettingsComponent.matchWeightFrequency.getIntSpinner().value as Int,
+ liveSettingsComponent.matchWeightRecency.getIntSpinner().value as Int
)
val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss, matchConfig)
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/FileAccessData.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/FileAccessData.kt
new file mode 100644
index 00000000..77caba74
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FileAccessData.kt
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Mitja Leino
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.mituuz.fuzzier.entities
+
+import java.io.Serializable
+
+class FileAccessData : Serializable {
+ var filePath: String = ""
+ var accessCount: Int = 0
+
+ constructor()
+
+ constructor(filePath: String, accessCount: Int) {
+ this.filePath = filePath
+ this.accessCount = accessCount
+ }
+}
+
+data class FileUsageStats(
+ val recentIndex: Int,
+ val accessCount: Int,
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
index fcad1371..ff4255d5 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
@@ -30,7 +30,6 @@ import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
import java.io.*
import java.util.*
-import javax.swing.DefaultListModel
class FuzzyMatchContainer(
val score: FuzzyScore,
@@ -111,10 +110,12 @@ class FuzzyMatchContainer(
var multiMatchScore = 0
var partialPathScore = 0
var filenameScore = 0
+ var frequencyScore = 0
+ var recencyScore = 0
val highlightCharacters: MutableSet = HashSet()
fun getTotalScore(): Int {
- return streakScore + multiMatchScore + partialPathScore + filenameScore
+ return streakScore + multiMatchScore + partialPathScore + filenameScore + frequencyScore + recencyScore
}
}
@@ -137,26 +138,15 @@ class FuzzyMatchContainer(
serialized.moduleBasePath = container.basePath
return serialized
}
-
- fun fromListModel(listModel: DefaultListModel): DefaultListModel {
- val serializedList = DefaultListModel()
- for (i in 0 until listModel.size) {
- serializedList.addElement(fromFuzzyMatchContainer(listModel[i]))
- }
- return serializedList
- }
-
- fun toListModel(serializedList: DefaultListModel): DefaultListModel {
- val listModel = DefaultListModel()
- for (i in 0 until serializedList.size) {
- listModel.addElement(serializedList[i].toFuzzyMatchContainer())
- }
- return listModel
- }
}
- fun toFuzzyMatchContainer(): FuzzyMatchContainer {
- return FuzzyMatchContainer(score!!, filePath!!, filename!!, moduleBasePath!!, FileType.FILE)
+ fun toFuzzyMatchContainer(): FuzzyMatchContainer? {
+ val score = score ?: return null
+ val filePath = filePath ?: return null
+ val filename = filename ?: return null
+ val moduleBasePath = moduleBasePath ?: return null
+
+ return FuzzyMatchContainer(score, filePath, filename, moduleBasePath, FileType.FILE)
}
var score: FuzzyScore? = null
@@ -177,21 +167,21 @@ class FuzzyMatchContainer(
*
* @see FuzzierSettingsService
*/
- class SerializedMatchContainerConverter : Converter>() {
- override fun fromString(value: String): DefaultListModel {
+ class SerializedMatchContainerConverter : Converter>() {
+ override fun fromString(value: String): List {
// Fallback to an empty list if deserialization fails
try {
val data = Base64.getDecoder().decode(value)
val byteArrayInputStream = ByteArrayInputStream(data)
@Suppress("UNCHECKED_CAST")
- return ObjectInputStream(byteArrayInputStream).use { it.readObject() as DefaultListModel }
+ return ObjectInputStream(byteArrayInputStream).use { it.readObject() as List }
} catch (_: Exception) {
- return DefaultListModel()
+ return listOf()
}
}
- override fun toString(value: DefaultListModel): String {
+ override fun toString(value: List): String {
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(value) }
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
index 7699f021..a57f7426 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
@@ -27,8 +27,10 @@ package com.mituuz.fuzzier.entities
data class MatchConfig(
val tolerance: Int = 0,
val multiMatch: Boolean = false,
- val matchWeightSingleChar: Int = 1,
- val matchWeightStreakModifier: Int = 5,
+ val matchWeightSingleChar: Int = 5,
+ val matchWeightStreakModifier: Int = 10,
val matchWeightPartialPath: Int = 10,
val matchWeightFilename: Int = 20,
+ val matchWeightFrequency: Int = 10,
+ val matchWeightRecency: Int = 10,
)
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
index e88d70f0..2e7a853a 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
@@ -28,7 +28,8 @@ import org.apache.commons.lang3.StringUtils
class ScoreCalculator(
searchString: String,
- private val config: MatchConfig
+ private val config: MatchConfig,
+ private val fileUsageStats: Map
) {
private val lowerSearchString: String = searchString.lowercase()
private val searchStringParts = lowerSearchString.split(" ")
@@ -82,9 +83,28 @@ class ScoreCalculator(
fuzzyScore.streakScore = (longestStreak * config.matchWeightStreakModifier) / 10
fuzzyScore.filenameScore = (longestFilenameStreak * config.matchWeightFilename) / 10
+ val fileStats = fileUsageStats[currentFilePath]
+ calculateUsageBoost(fileStats)
+
return fuzzyScore
}
+ internal fun calculateUsageBoost(fileStats: FileUsageStats?): Int {
+ if (fileStats == null) {
+ return 0
+ }
+
+ val recencyBoost = ((10 - fileStats.recentIndex / 2).coerceAtLeast(0) * config.matchWeightRecency) / 10
+ val frequencyBoost = ((fileStats.accessCount / 5).coerceAtMost(10) * config.matchWeightFrequency) / 10
+
+ if (this::fuzzyScore.isInitialized) {
+ fuzzyScore.recencyScore = recencyBoost
+ fuzzyScore.frequencyScore = frequencyBoost
+ }
+
+ return recencyBoost + frequencyBoost
+ }
+
/**
* Returns false if no match can be found, this stops the search
*/
@@ -148,7 +168,7 @@ class ScoreCalculator(
longestFilenameStreak = 0
filePathIndex = filenameIndex
- while (searchStringIndex < searchStringLength && filePathIndex < currentFilePath.length) {
+ while (searchStringIndex < lowerSearchString.length && filePathIndex < currentFilePath.length) {
processFilenameChar(lowerSearchString[searchStringIndex])
}
}
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
index d63fc673..34910155 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
@@ -32,13 +32,14 @@ import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FileType
class StringEvaluator(
private var exclusionList: Set,
private var modules: Map,
+ private val fileUsageStats: Map
) {
fun evaluateIteratorEntry(
iteratorEntry: IterationEntry,
searchString: String,
matchConfig: MatchConfig
): FuzzyMatchContainer? {
- val scoreCalculator = ScoreCalculator(searchString, matchConfig)
+ val scoreCalculator = ScoreCalculator(searchString, matchConfig, fileUsageStats)
val moduleName = iteratorEntry.module
val moduleBasePath = modules[moduleName] ?: return null
diff --git a/src/main/kotlin/com/mituuz/fuzzier/search/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/search/Fuzzier.kt
index ad402980..f2ced081 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/search/Fuzzier.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/search/Fuzzier.kt
@@ -144,7 +144,8 @@ open class Fuzzier : FilesystemAction() {
addFileToRecentlySearchedFiles(
selectedValue,
projectState,
- globalState
+ globalState.fileListLimit,
+ globalState.fileMetadataCacheSize
)
}
popup.cancel()
diff --git a/src/main/kotlin/com/mituuz/fuzzier/search/initialview/DefaultInitialListModelProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/search/initialview/DefaultInitialListModelProvider.kt
index 57ded887..aea3cc92 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/search/initialview/DefaultInitialListModelProvider.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/search/initialview/DefaultInitialListModelProvider.kt
@@ -45,7 +45,7 @@ class DefaultInitialListModelProvider(
}
FuzzierGlobalSettingsService.RecentFilesMode.RECENTLY_SEARCHED_FILES -> {
- getRecentlySearchedFiles()
+ getRecentlySearchedFiles(projectState)
}
else -> {
@@ -82,16 +82,8 @@ class DefaultInitialListModelProvider(
return listModel
}
- fun getRecentlySearchedFiles(): DefaultListModel {
- val result = DefaultListModel()
- projectState.getRecentlySearchedFilesAsFuzzyMatchContainer()
- .elements()
- .toList()
- .filterNotNull()
- .reversed()
- .let {
- result.addAll(it)
- }
- return result
- }
+ fun getRecentlySearchedFiles(state: FuzzierSettingsService.State): DefaultListModel =
+ DefaultListModel().apply {
+ state.recentlySearchedFiles?.asReversed()?.forEach { addElement(it.toFuzzyMatchContainer()) }
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtil.kt
index 25001289..201f612f 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtil.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtil.kt
@@ -24,36 +24,75 @@
package com.mituuz.fuzzier.search.initialview
+import com.mituuz.fuzzier.entities.FileAccessData
import com.mituuz.fuzzier.entities.FuzzyContainer
import com.mituuz.fuzzier.entities.FuzzyMatchContainer
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
+import com.mituuz.fuzzier.entities.FuzzyMatchContainer.SerializedMatchContainer.Companion.fromFuzzyMatchContainer
import com.mituuz.fuzzier.settings.FuzzierSettingsService
-import javax.swing.DefaultListModel
+/**
+ * Adds a file to the list of recently searched files while ensuring that the list does not exceed
+ * the specified limit, maintains uniqueness, and updates the file metadata cache.
+ *
+ * @param incomingContainer The container representing the file to be added to the recently
+ * searched files list.
+ * @param projectState The current state of the project, which holds the recently searched
+ * files and related metadata.
+ * @param fileListLimit The maximum number of files that can be maintained in the list of
+ * recently searched files.
+ * @param fileMetadataCacheSize The maximum size allowed for the file metadata cache.
+ */
fun addFileToRecentlySearchedFiles(
- fuzzyContainer: FuzzyContainer,
+ incomingContainer: FuzzyContainer,
projectState: FuzzierSettingsService.State,
- globalState: FuzzierGlobalSettingsService.State
+ fileListLimit: Int,
+ fileMetadataCacheSize: Int,
) {
- val listModel: DefaultListModel =
- projectState.getRecentlySearchedFilesAsFuzzyMatchContainer()
+ val recentFiles: MutableList =
+ projectState.recentlySearchedFiles?.mapNotNull { it.toFuzzyMatchContainer() }?.toMutableList()
+ ?: mutableListOf()
var i = 0
- while (i < listModel.size) {
- if (listModel[i].filePath == fuzzyContainer.filePath) {
- listModel.remove(i)
+ while (i < recentFiles.size) {
+ if (recentFiles[i].filePath == incomingContainer.filePath) {
+ recentFiles.removeAt(i)
} else {
i++
}
}
- while (listModel.size > globalState.fileListLimit - 1) {
- listModel.remove(listModel.size - 1)
+ while (recentFiles.size > fileListLimit - 1) {
+ recentFiles.removeAt(recentFiles.size - 1)
}
- if (fuzzyContainer is FuzzyMatchContainer) {
- listModel.addElement(fuzzyContainer)
- projectState.recentlySearchedFiles =
- FuzzyMatchContainer.SerializedMatchContainer.fromListModel(listModel)
+ if (incomingContainer is FuzzyMatchContainer) {
+ recentFiles.add(incomingContainer)
+ projectState.recentlySearchedFiles = recentFiles.map { fromFuzzyMatchContainer(it) }
}
+
+ projectState.recentFiles = addFileToLRUCache(
+ incomingContainer, projectState.recentFiles, fileMetadataCacheSize
+ )
+}
+
+fun addFileToLRUCache(
+ incomingContainer: FuzzyContainer, recentFiles: MutableList, maxSize: Int
+): MutableList {
+ val lower = incomingContainer.filePath.lowercase()
+ val existingIndex = recentFiles.indexOfFirst { it.filePath.lowercase() == lower }
+
+ val existingEntry = if (existingIndex != -1) recentFiles.removeAt(existingIndex) else null
+
+ val newEntry = FileAccessData(
+ filePath = lower,
+ accessCount = (existingEntry?.accessCount ?: 0) + 1
+ )
+
+ recentFiles.add(0, newEntry)
+
+ while (recentFiles.size > maxSize) {
+ recentFiles.removeLast()
+ }
+
+ return recentFiles
}
diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
index 25b42249..85c5d7b9 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
@@ -84,6 +84,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
component.matchWeightSingleChar.getIntSpinner().isEnabled = state.multiMatch
component.matchWeightStreakModifier.getIntSpinner().value = state.matchWeightStreakModifier
component.matchWeightFilename.getIntSpinner().value = state.matchWeightFilename
+ component.matchWeightFrequency.getIntSpinner().value = state.matchWeightFrequency
+ component.matchWeightRecency.getIntSpinner().value = state.matchWeightRecency
return component.jPanel
}
@@ -129,6 +131,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
|| state.matchWeightSingleChar != component.matchWeightSingleChar.getIntSpinner().value
|| state.matchWeightStreakModifier != component.matchWeightStreakModifier.getIntSpinner().value
|| state.matchWeightFilename != component.matchWeightFilename.getIntSpinner().value
+ || state.matchWeightFrequency != component.matchWeightFrequency.getIntSpinner().value
+ || state.matchWeightRecency != component.matchWeightRecency.getIntSpinner().value
|| state.globalExclusionSet != newGlobalSet
|| state.grepBackend != component.grepBackendSelector.getGrepBackendComboBox().selectedItem
}
@@ -176,6 +180,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
state.matchWeightSingleChar = component.matchWeightSingleChar.getIntSpinner().value as Int
state.matchWeightStreakModifier = component.matchWeightStreakModifier.getIntSpinner().value as Int
state.matchWeightFilename = component.matchWeightFilename.getIntSpinner().value as Int
+ state.matchWeightFrequency = component.matchWeightFrequency.getIntSpinner().value as Int
+ state.matchWeightRecency = component.matchWeightRecency.getIntSpinner().value as Int
val newGlobalSet = component.globalExclusionTextArea.text
.lines()
diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
index e8fac6d5..3f30251b 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
@@ -61,6 +61,7 @@ class FuzzierGlobalSettingsService : PersistentStateComponent = emptySet()
@@ -70,6 +71,8 @@ class FuzzierGlobalSettingsService : PersistentStateComponent {
class State {
+ /** Map of module identifiers and base paths. */
var modules: Map = HashMap()
+
+ /** Flag indicating if we should use ProjectFileIndex or modules for file iteration. */
var isProject = false
+
+ /** List of recently searched files. */
@OptionTag(converter = FuzzyMatchContainer.SerializedMatchContainerConverter::class)
- var recentlySearchedFiles: DefaultListModel? = DefaultListModel()
+ var recentlySearchedFiles: List? = listOf()
+
+ /** List of recently searched files. */
+ var recentFiles: MutableList = mutableListOf()
+ /** Set of file patterns to be excluded from searches. */
var exclusionSet: Set = setOf("/.idea/*", "/.git/*", "/target/*", "/build/*", "/.gradle/*", "/.run/*")
- var ignoredCharacters: String = ""
- fun getRecentlySearchedFilesAsFuzzyMatchContainer(): DefaultListModel {
- val list = recentlySearchedFiles ?: DefaultListModel()
- return FuzzyMatchContainer.SerializedMatchContainer.toListModel(list)
- }
+ /** Characters to ignore during text matching. */
+ var ignoredCharacters: String = ""
}
private var state = State()
diff --git a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt
index c6786b3c..b439f031 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt
@@ -38,7 +38,7 @@ class StringEvaluatorTest {
private val moduleBasePath = "/m1/src"
private fun evaluate(filePaths: List, exclusionList: Set): List {
- val evaluator = StringEvaluator(exclusionList, mapOf(moduleName to moduleBasePath))
+ val evaluator = StringEvaluator(exclusionList, mapOf(moduleName to moduleBasePath), mapOf())
return filePaths.mapNotNull { fp ->
// Build absolute path under a fake module root so that removePrefix(moduleBasePath) works like in production
diff --git a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
index ba089fb0..25d1e8aa 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
@@ -34,7 +34,6 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
-import javax.swing.DefaultListModel
class FuzzyMatchContainerTest {
@Suppress("unused")
@@ -103,34 +102,33 @@ class FuzzyMatchContainerTest {
val deserialized =
ObjectInputStream(byteArrayInputStream).use { it.readObject() as FuzzyMatchContainer.SerializedMatchContainer }
val fmc = deserialized.toFuzzyMatchContainer()
- assertEquals("", fmc.filePath)
- assertEquals("FuzzyMatchContainerTest.kt", fmc.filename)
+ assertEquals("", fmc?.filePath)
+ assertEquals("FuzzyMatchContainerTest.kt", fmc?.filename)
}
@Test
fun `Test default list serialization`() {
- val list = DefaultListModel()
val score = FuzzyScore()
val container = FuzzyMatchContainer(
score, "", "FuzzyMatchContainerTest.kt", "",
FILE
)
- list.addElement(container)
+ val list = listOf(FuzzyMatchContainer.SerializedMatchContainer.fromFuzzyMatchContainer(container))
val converter = FuzzyMatchContainer.SerializedMatchContainerConverter()
- val stringRep = converter.toString(FuzzyMatchContainer.SerializedMatchContainer.fromListModel(list))
+ val stringRep = converter.toString(list)
- val deserialized: DefaultListModel =
+ val deserialized: List =
converter.fromString(stringRep)
assertEquals(1, deserialized.size)
- assertEquals("", deserialized.get(0).filePath)
- assertEquals("FuzzyMatchContainerTest.kt", deserialized.get(0).filename)
+ assertEquals("", deserialized[0].filePath)
+ assertEquals("FuzzyMatchContainerTest.kt", deserialized[0].filename)
}
@Test
fun `Deserialization fails`() {
val converter = FuzzyMatchContainer.SerializedMatchContainerConverter()
- val deserialized: DefaultListModel =
+ val deserialized: List =
converter.fromString("This should not work")
assertEquals(0, deserialized.size)
}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
index 687a0432..bc281916 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
@@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test
class ScoreCalculatorTest {
@Test
fun `Search string contained same index`() {
- val sc = ScoreCalculator("test", MatchConfig())
+ val sc = ScoreCalculator("test", MatchConfig(), mapOf())
sc.searchStringIndex = 0
sc.searchStringLength = 4
@@ -41,7 +41,7 @@ class ScoreCalculatorTest {
@Test
fun `Search string contained different index`() {
- val sc = ScoreCalculator("test", MatchConfig())
+ val sc = ScoreCalculator("test", MatchConfig(), mapOf())
sc.searchStringIndex = 0
sc.searchStringLength = 4
@@ -56,7 +56,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/test")
assertEquals(4, fScore!!.streakScore)
@@ -67,7 +67,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/te/st")
assertEquals(2, fScore!!.streakScore)
@@ -78,7 +78,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/te")
assertNull(fScore)
@@ -89,7 +89,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightSingleChar = 10, multiMatch = true
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/test")
assertEquals(4, fScore!!.multiMatchScore)
@@ -100,7 +100,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightSingleChar = 10, multiMatch = true
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/testtest")
assertEquals(8, fScore!!.multiMatchScore)
}
@@ -110,7 +110,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightSingleChar = 10, multiMatch = true
)
- val sc = ScoreCalculator("test test", matchConfig)
+ val sc = ScoreCalculator("test test", matchConfig, mapOf())
val fScore = sc.calculateScore("/testtest")
assertEquals(8, fScore!!.multiMatchScore)
}
@@ -120,7 +120,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightPartialPath = 1
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/test.kt")
assertEquals(1, fScore!!.partialPathScore)
}
@@ -130,14 +130,14 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightFilename = 10
)
- val sc = ScoreCalculator("test", matchConfig)
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
val fScore = sc.calculateScore("/test.kt")
assertEquals(4, fScore!!.filenameScore)
}
@Test
fun `Empty ss and fp`() {
- val sc = ScoreCalculator("", MatchConfig())
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
val fScore = sc.calculateScore("")
assertEquals(0, fScore!!.getTotalScore())
@@ -149,7 +149,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kif", matchConfig)
+ val sc = ScoreCalculator("kif", matchConfig, mapOf())
val fScore = sc.calculateScore("/KotlinIsFun")
@@ -164,7 +164,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kot", matchConfig)
+ val sc = ScoreCalculator("kot", matchConfig, mapOf())
val fScore = sc.calculateScore("/KotlinIsFun")
@@ -176,21 +176,21 @@ class ScoreCalculatorTest {
@Test
fun `Too long ss`() {
- val sc = ScoreCalculator("TooLongSearchString", MatchConfig())
+ val sc = ScoreCalculator("TooLongSearchString", MatchConfig(), mapOf())
val fScore = sc.calculateScore("/KIF")
assertNull(fScore)
}
@Test
fun `No possible match`() {
- val sc = ScoreCalculator("A", MatchConfig())
+ val sc = ScoreCalculator("A", MatchConfig(), mapOf())
val fScore = sc.calculateScore("/KIF")
assertNull(fScore)
}
@Test
fun `Empty ss`() {
- val sc = ScoreCalculator("", MatchConfig())
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
val fScore = sc.calculateScore("/KIF")
assertEquals(0, fScore!!.getTotalScore())
@@ -198,14 +198,14 @@ class ScoreCalculatorTest {
@Test
fun `No possible match split`() {
- val sc = ScoreCalculator("A A B", MatchConfig())
+ val sc = ScoreCalculator("A A B", MatchConfig(), mapOf())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
assertNull(fScore)
}
@Test
fun `Partial match split`() {
- val sc = ScoreCalculator("A A K", MatchConfig())
+ val sc = ScoreCalculator("A A K", MatchConfig(), mapOf())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
assertNull(fScore)
}
@@ -215,7 +215,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("fun kotlin", matchConfig)
+ val sc = ScoreCalculator("fun kotlin", matchConfig, mapOf())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -230,7 +230,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kif", matchConfig)
+ val sc = ScoreCalculator("kif", matchConfig, mapOf())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -245,7 +245,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kif", matchConfig)
+ val sc = ScoreCalculator("kif", matchConfig, mapOf())
val fScore = sc.calculateScore("/Kiffer/Is/Fun/kiffer.kt")
@@ -260,7 +260,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kif", matchConfig)
+ val sc = ScoreCalculator("kif", matchConfig, mapOf())
val fScore = sc.calculateScore("/Kif/Is/Fun/kif.kt")
@@ -275,7 +275,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
)
- val sc = ScoreCalculator("kif fun kotlin", matchConfig)
+ val sc = ScoreCalculator("kif fun kotlin", matchConfig, mapOf())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -290,7 +290,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 5,
)
- val sc = ScoreCalculator("kotlin", matchConfig)
+ val sc = ScoreCalculator("kotlin", matchConfig, mapOf())
assertNotNull(sc.calculateScore("/Kotlin"))
}
@@ -300,7 +300,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 5,
)
- val sc = ScoreCalculator("korlin", matchConfig)
+ val sc = ScoreCalculator("korlin", matchConfig, mapOf())
assertNotNull(sc.calculateScore("/Kotlin"))
}
@@ -310,7 +310,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 1,
)
- val sc = ScoreCalculator("korlin", matchConfig)
+ val sc = ScoreCalculator("korlin", matchConfig, mapOf())
assertNotNull(sc.calculateScore("/Kotlin"))
}
@@ -320,7 +320,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 1,
)
- val sc = ScoreCalculator("korlnn", matchConfig)
+ val sc = ScoreCalculator("korlnn", matchConfig, mapOf())
assertNull(sc.calculateScore("/Kotlin"))
}
@@ -330,7 +330,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 1,
)
- val sc = ScoreCalculator("korlin", matchConfig)
+ val sc = ScoreCalculator("korlin", matchConfig, mapOf())
assertNotNull(sc.calculateScore("/Kot/lin"))
}
@@ -340,7 +340,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 1,
)
- val sc = ScoreCalculator("korlin", matchConfig)
+ val sc = ScoreCalculator("korlin", matchConfig, mapOf())
assertNull(sc.calculateScore("/Kot/sin"))
}
@@ -350,7 +350,7 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 2,
)
- val sc = ScoreCalculator("korlin", matchConfig)
+ val sc = ScoreCalculator("korlin", matchConfig, mapOf())
assertNotNull(sc.calculateScore("/Kot/sin"))
}
@@ -360,8 +360,151 @@ class ScoreCalculatorTest {
val matchConfig = MatchConfig(
tolerance = 5,
)
- val sc = ScoreCalculator("kotlin12345", matchConfig)
+ val sc = ScoreCalculator("kotlin12345", matchConfig, mapOf())
assertNull(sc.calculateScore("/Kotlin"))
}
+
+
+ @Test
+ fun `Usage boost with null stats`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(0, sc.calculateUsageBoost(null))
+ }
+
+ @Test
+ fun `Usage boost with recent index 0`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(10, sc.calculateUsageBoost(FileUsageStats(0, 0)))
+ }
+
+ @Test
+ fun `Usage boost with recent index 10`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(5, sc.calculateUsageBoost(FileUsageStats(10, 0)))
+ }
+
+ @Test
+ fun `Usage boost with recent index 20`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(0, sc.calculateUsageBoost(FileUsageStats(20, 0)))
+ }
+
+ @Test
+ fun `Usage boost with access count`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(11, sc.calculateUsageBoost(FileUsageStats(0, 5)))
+ }
+
+ @Test
+ fun `Usage boost affects total score`() {
+ val fileUsageStats = mapOf("/test.kt" to FileUsageStats(0, 5))
+ val sc = ScoreCalculator("test", MatchConfig(), fileUsageStats)
+ val fScore = sc.calculateScore("/test.kt")
+ assertNotNull(fScore)
+ assertEquals(1, fScore!!.frequencyScore)
+ assertEquals(10, fScore.recencyScore)
+ // streakScore = (4 * 10) / 10 = 4
+ // filenameScore = (4 * 20) / 10 = 8
+ // partialPathScore = 10 (default weight)
+ // total = 1 + 10 + 4 + 8 + 10 = 33
+ assertEquals(33, fScore.getTotalScore())
+ }
+
+ @Test
+ fun `Usage boost affects total score with weights`() {
+ val fileUsageStats = mapOf("/test.kt" to FileUsageStats(0, 5))
+ val matchConfig = MatchConfig(matchWeightFrequency = 20, matchWeightRecency = 5)
+ val sc = ScoreCalculator("test", matchConfig, fileUsageStats)
+ val fScore = sc.calculateScore("/test.kt")
+ assertNotNull(fScore)
+ // Frequency boost: (1 * 20) / 10 = 2
+ // Recency boost: (10 * 5) / 10 = 5
+ assertEquals(2, fScore!!.frequencyScore)
+ assertEquals(5, fScore.recencyScore)
+ // streakScore = (4 * 10) / 10 = 4
+ // filenameScore = (4 * 20) / 10 = 8
+ // partialPathScore = 10 (default weight)
+ // total = 2 + 5 + 4 + 8 + 10 = 29
+ assertEquals(29, fScore.getTotalScore())
+ }
+
+ @Test
+ fun `Recency boost is smooth`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ assertEquals(10, sc.calculateUsageBoost(FileUsageStats(0, 0)))
+ assertEquals(9, sc.calculateUsageBoost(FileUsageStats(2, 0)))
+ assertEquals(8, sc.calculateUsageBoost(FileUsageStats(4, 0)))
+ assertEquals(5, sc.calculateUsageBoost(FileUsageStats(10, 0)))
+ assertEquals(0, sc.calculateUsageBoost(FileUsageStats(20, 0)))
+ assertEquals(0, sc.calculateUsageBoost(FileUsageStats(30, 0)))
+ }
+
+ @Test
+ fun `Not in recent list is not better than late in recent list`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+
+ val notInListBoost = sc.calculateUsageBoost(null) // 0
+ val lateInListBoost = sc.calculateUsageBoost(FileUsageStats(20, 0)) // 0
+
+ assertEquals(notInListBoost, lateInListBoost, "File NOT in list should be equal to a file that is very old in the list")
+ }
+
+ @Test
+ fun `Frequency boost is capped`() {
+ val sc = ScoreCalculator("", MatchConfig(), mapOf())
+ // 100 accesses gives 10 points (capped)
+ // index 10 gives 5 points
+ assertEquals(15, sc.calculateUsageBoost(FileUsageStats(10, 100)))
+ }
+
+ @Test
+ fun `Usage boost with weights set to 0`() {
+ val matchConfig = MatchConfig(matchWeightFrequency = 0, matchWeightRecency = 0)
+ val sc = ScoreCalculator("", matchConfig, mapOf())
+ assertEquals(0, sc.calculateUsageBoost(FileUsageStats(0, 100)))
+ }
+
+ @Test
+ fun `Multi match weight affects total score`() {
+ val matchConfig = MatchConfig(
+ multiMatch = true,
+ matchWeightSingleChar = 20
+ )
+ val sc = ScoreCalculator("test", matchConfig, mapOf())
+ val fScore = sc.calculateScore("/test.kt")
+ assertNotNull(fScore)
+
+ // "test.kt" contains 't', 'e', 's', 't', 't' -> 5 matches.
+ // weight 20. (5 * 20) / 10 = 10.
+ assertEquals(10, fScore!!.multiMatchScore)
+ }
+
+ @Test
+ fun `All weights affect total score`() {
+ val fileUsageStats = mapOf("/test.kt" to FileUsageStats(0, 5))
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 20,
+ matchWeightFilename = 30,
+ matchWeightPartialPath = 5,
+ matchWeightFrequency = 20,
+ matchWeightRecency = 5
+ )
+ val sc = ScoreCalculator("test", matchConfig, fileUsageStats)
+ val fScore = sc.calculateScore("/test.kt")
+ assertNotNull(fScore)
+
+ // streakScore: longestStreak=4, weight=20. (4 * 20) / 10 = 8
+ // filenameScore: longestFilenameStreak=4, weight=30. (4 * 30) / 10 = 12
+ // partialPathScore: weight=5. 5
+ // frequencyScore: access=5, weight=20. (1 * 20) / 10 = 2
+ // recencyScore: index=0, weight=5. (10 * 5) / 10 = 5
+
+ assertEquals(8, fScore!!.streakScore)
+ assertEquals(12, fScore.filenameScore)
+ assertEquals(5, fScore.partialPathScore)
+ assertEquals(2, fScore.frequencyScore)
+ assertEquals(5, fScore.recencyScore)
+ assertEquals(8 + 12 + 5 + 2 + 5, fScore.getTotalScore()) // 32
+ }
}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtilTest.kt b/src/test/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtilTest.kt
index d7ff4831..5a4c5e6a 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtilTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/search/initialview/RecentlySearchedFilesUtilTest.kt
@@ -26,93 +26,153 @@ package com.mituuz.fuzzier.search.initialview
import com.intellij.openapi.components.service
import com.intellij.testFramework.TestApplicationManager
+import com.mituuz.fuzzier.entities.FileAccessData
import com.mituuz.fuzzier.entities.FuzzyMatchContainer
import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FileType.FILE
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
import io.mockk.unmockkAll
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import javax.swing.DefaultListModel
class RecentlySearchedFilesUtilTest {
@Suppress("unused") // Required for add to recently used files (fuzzierSettingsServiceInstance)
private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
+ private lateinit var fuzzierSettingsServiceInstance: FuzzierSettingsService
+
+ @BeforeEach
+ fun setUp() {
+ fuzzierSettingsServiceInstance = service()
+ fuzzierSettingsServiceInstance.state.recentlySearchedFiles = mutableListOf()
+ }
@AfterEach
fun tearDown() {
unmockkAll()
}
+ private fun createContainer(path: String = ""): FuzzyMatchContainer {
+ return FuzzyMatchContainer(FuzzyMatchContainer.FuzzyScore(), path, path, path, FILE)
+ }
+
@Test
fun `Add file to recently used files - Null list should default to empty`() {
- val fuzzierSettingsServiceInstance: FuzzierSettingsService = service()
- val fgss = service().state
- val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "", FILE)
+ val container = createContainer()
fuzzierSettingsServiceInstance.state.recentlySearchedFiles = null
addFileToRecentlySearchedFiles(
container,
fuzzierSettingsServiceInstance.state,
- fgss
+ 20, 100
)
- assertNotNull(fuzzierSettingsServiceInstance.state.getRecentlySearchedFilesAsFuzzyMatchContainer())
- assertEquals(1, fuzzierSettingsServiceInstance.state.getRecentlySearchedFilesAsFuzzyMatchContainer().size)
+ assertNotNull(fuzzierSettingsServiceInstance.state.recentlySearchedFiles)
+ assertEquals(1, fuzzierSettingsServiceInstance.state.recentlySearchedFiles?.size)
}
@Test
fun `Add file to recently used files - Too large list is truncated`() {
- val fuzzierSettingsServiceInstance: FuzzierSettingsService = service()
- val fgss = service().state
val fileListLimit = 2
- val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "", FILE)
+ val container = createContainer()
- val largeList: DefaultListModel = DefaultListModel()
+ val largeList: MutableList = mutableListOf()
for (i in 0..25) {
- largeList.addElement(FuzzyMatchContainer(score, "" + i, "" + i, "", FILE))
+ largeList.add(createContainer("" + i))
}
- fgss.fileListLimit = fileListLimit
-
fuzzierSettingsServiceInstance.state.recentlySearchedFiles =
- FuzzyMatchContainer.SerializedMatchContainer.fromListModel(largeList)
+ largeList.map { FuzzyMatchContainer.SerializedMatchContainer.fromFuzzyMatchContainer(it) }
addFileToRecentlySearchedFiles(
container,
fuzzierSettingsServiceInstance.state,
- fgss
+ fileListLimit, 100
)
assertEquals(
fileListLimit,
- fuzzierSettingsServiceInstance.state.getRecentlySearchedFilesAsFuzzyMatchContainer().size
+ fuzzierSettingsServiceInstance.state.recentlySearchedFiles?.size
)
}
@Test
fun `Add file to recently used files - Duplicate filenames are removed`() {
- val fuzzierSettingsServiceInstance: FuzzierSettingsService = service()
- val fgss = service().state
- val fileListLimit = 20
- val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "", FILE)
+ val container = createContainer()
- val largeList: DefaultListModel = DefaultListModel()
+ val largeList: MutableList = mutableListOf()
repeat(26) {
- largeList.addElement(FuzzyMatchContainer(score, "", "", "", FILE))
+ largeList.add(createContainer())
}
- fgss.fileListLimit = fileListLimit
-
fuzzierSettingsServiceInstance.state.recentlySearchedFiles =
- FuzzyMatchContainer.SerializedMatchContainer.fromListModel(largeList)
+ largeList.map { FuzzyMatchContainer.SerializedMatchContainer.fromFuzzyMatchContainer(it) }
addFileToRecentlySearchedFiles(
container,
fuzzierSettingsServiceInstance.state,
- fgss
+ 20, 100
)
- assertEquals(1, fuzzierSettingsServiceInstance.state.getRecentlySearchedFilesAsFuzzyMatchContainer().size)
+ assertEquals(1, fuzzierSettingsServiceInstance.state.recentlySearchedFiles?.size)
+ }
+
+ @Test
+ fun `addFileToLRUCache - Add new file to empty cache`() {
+ val recentFiles = mutableListOf()
+ val container = createContainer("path1")
+ val result = addFileToLRUCache(container, recentFiles, 5)
+
+ assertEquals(1, result.size)
+ assertEquals("path1", result[0].filePath)
+ assertEquals(1, result[0].accessCount)
+ }
+
+ @Test
+ fun `addFileToLRUCache - Add new file to non-empty cache`() {
+ val recentFiles = mutableListOf(
+ FileAccessData("path1", 1)
+ )
+ val container = createContainer("path2")
+ val result = addFileToLRUCache(container, recentFiles, 5)
+
+ assertEquals(2, result.size)
+ assertEquals("path2", result[0].filePath)
+ assertEquals(1, result[0].accessCount)
+ assertEquals("path1", result[1].filePath)
+ }
+
+ @Test
+ fun `addFileToLRUCache - Add existing file`() {
+ val recentFiles = mutableListOf(
+ FileAccessData("path1", 1),
+ FileAccessData("path2", 1)
+ )
+ val container = createContainer("path2")
+ val result = addFileToLRUCache(container, recentFiles, 5)
+
+ assertEquals(2, result.size)
+ assertEquals("path2", result[0].filePath)
+ assertEquals(2, result[0].accessCount)
+ assertEquals("path1", result[1].filePath)
+ }
+
+ @Test
+ fun `addFileToLRUCache - Exceeding max size`() {
+ val recentFiles = mutableListOf(
+ FileAccessData("path2", 1),
+ FileAccessData("path1", 1)
+ )
+ val container = createContainer("path3")
+ val result = addFileToLRUCache(container, recentFiles, 2)
+
+ assertEquals(2, result.size)
+ assertEquals("path3", result[0].filePath)
+ assertEquals("path2", result[1].filePath)
+ }
+
+ @Test
+ fun `addFileToLRUCache - Max size 0`() {
+ val recentFiles = mutableListOf()
+ val container = createContainer("path1")
+ val result = addFileToLRUCache(container, recentFiles, 0)
+
+ assertEquals(0, result.size)
}
}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
index 39a42cf2..8110227d 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
@@ -62,6 +62,8 @@ class FuzzierGlobalSettingsConfigurableTest {
state.matchWeightSingleChar = 6
state.matchWeightStreakModifier = 20
state.matchWeightFilename = 15
+ state.matchWeightFrequency = 12
+ state.matchWeightRecency = 13
}
@Test
@@ -270,6 +272,20 @@ class FuzzierGlobalSettingsConfigurableTest {
assertTrue(settingsConfigurable.isModified)
}
+ @Test
+ fun matchWeightFrequency() {
+ pre()
+ state.matchWeightFrequency = 13
+ assertTrue(settingsConfigurable.isModified)
+ }
+
+ @Test
+ fun matchWeightRecency() {
+ pre()
+ state.matchWeightRecency = 14
+ assertTrue(settingsConfigurable.isModified)
+ }
+
@Test
fun grepBackend_isPopulatedFromState() {
state.grepBackend = FuzzierGlobalSettingsService.GrepBackend.FUZZIER
diff --git a/src/test/kotlin/com/mituuz/fuzzier/util/DefaultInitialListModelProviderTest.kt b/src/test/kotlin/com/mituuz/fuzzier/util/DefaultInitialListModelProviderTest.kt
index dec7861e..d71c20c6 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/util/DefaultInitialListModelProviderTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/util/DefaultInitialListModelProviderTest.kt
@@ -28,6 +28,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.TestApplicationManager
import com.mituuz.fuzzier.entities.FuzzyMatchContainer
+import com.mituuz.fuzzier.entities.FuzzyMatchContainer.SerializedMatchContainer.Companion.fromFuzzyMatchContainer
import com.mituuz.fuzzier.search.initialview.DefaultInitialListModelProvider
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
@@ -40,15 +41,12 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import javax.swing.DefaultListModel
class DefaultInitialListModelProviderTest {
private lateinit var project: Project
private lateinit var fuzzierSettingsService: FuzzierSettingsService
private lateinit var fuzzierGlobalSettingsService: FuzzierGlobalSettingsService
- private lateinit var fuzzierUtil: FuzzierUtil
private lateinit var defaultInitialListModelProvider: DefaultInitialListModelProvider
- private lateinit var state: State
private lateinit var editorHistoryManager: EditorHistoryManager
@Suppress("unused") // Required for add to recently used files (fuzzierSettingsServiceInstance)
@@ -59,12 +57,10 @@ class DefaultInitialListModelProviderTest {
project = mockk()
fuzzierSettingsService = mockk()
fuzzierGlobalSettingsService = mockk()
- state = mockk()
val globalState = FuzzierGlobalSettingsService.State()
globalState.recentFilesMode = FuzzierGlobalSettingsService.RecentFilesMode.RECENT_PROJECT_FILES
- defaultInitialListModelProvider = DefaultInitialListModelProvider(project, globalState, state)
+ defaultInitialListModelProvider = DefaultInitialListModelProvider(project, globalState, State())
editorHistoryManager = mockk()
- every { fuzzierSettingsService.state } returns state
}
@AfterEach
@@ -90,7 +86,7 @@ class DefaultInitialListModelProviderTest {
every { virtualFile2.path } returns "/project/path/file2"
every { virtualFile2.name } returns "filename2"
- val settingsState = FuzzierSettingsService.State()
+ val settingsState = State()
settingsState.modules = mapOf("module" to "/project/path/")
defaultInitialListModelProvider = DefaultInitialListModelProvider(project, fgss, settingsState)
@@ -118,7 +114,7 @@ class DefaultInitialListModelProviderTest {
every { virtualFile2.path } returns "/other/path/file2"
every { virtualFile2.name } returns "filename2"
- val settingsState = FuzzierSettingsService.State()
+ val settingsState = State()
settingsState.modules = mapOf("module" to "/project/path/")
defaultInitialListModelProvider = DefaultInitialListModelProvider(project, fgss, settingsState)
@@ -143,31 +139,40 @@ class DefaultInitialListModelProviderTest {
}
@Test
- fun `Recently searched files - Order of multiple files`() {
- val fuzzyMatchContainer1 = mockk()
- val fuzzyMatchContainer2 = mockk()
- val listModel = DefaultListModel()
- listModel.addElement(fuzzyMatchContainer1)
- listModel.addElement(fuzzyMatchContainer2)
- every { state.getRecentlySearchedFilesAsFuzzyMatchContainer() } returns listModel
-
- val result = defaultInitialListModelProvider.getRecentlySearchedFiles()
-
- assertEquals(fuzzyMatchContainer2, result[0])
- assertEquals(fuzzyMatchContainer1, result[1])
- }
+ fun `getRecentlySearchedFiles returns recently searched files in reverse order`() {
+ val state = State()
+ state.recentlySearchedFiles = listOf(
+ fromFuzzyMatchContainer(
+ FuzzyMatchContainer(
+ FuzzyMatchContainer.FuzzyScore(),
+ "/old.kt",
+ "old.kt",
+ "/module",
+ FuzzyMatchContainer.FileType.FILE
+ )
+ ),
+ fromFuzzyMatchContainer(
+ FuzzyMatchContainer(
+ FuzzyMatchContainer.FuzzyScore(),
+ "/new.kt",
+ "new.kt",
+ "/module",
+ FuzzyMatchContainer.FileType.FILE
+ )
+ ),
+ )
- @Test
- fun `Recently searched files - Remove null elements from the list`() {
- val fuzzyMatchContainer = mockk()
- val listModel = DefaultListModel()
- listModel.addElement(fuzzyMatchContainer)
- listModel.addElement(null)
- listModel.addElement(null)
- every { state.getRecentlySearchedFilesAsFuzzyMatchContainer() } returns listModel
+ val model = defaultInitialListModelProvider.getRecentlySearchedFiles(state)
- val result = defaultInitialListModelProvider.getRecentlySearchedFiles()
+ assertEquals("new.kt", model.getElementAt(0).filename)
+ assertEquals("old.kt", model.getElementAt(1).filename)
+ }
- assertEquals(1, result.size)
+ @Test
+ fun `Recently searched files - No files`() {
+ val state = State()
+ state.recentlySearchedFiles = mutableListOf()
+ val result = defaultInitialListModelProvider.getRecentlySearchedFiles(state)
+ assertEquals(0, result.size())
}
}
\ No newline at end of file