Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ libs/
!**/src/main/**/build/
!**/src/test/**/build/
.aider*
.junie/

### IntelliJ IDEA ###
.intellijPlatform
Expand Down
21 changes: 10 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,16 +41,15 @@ intellijPlatform {
<h2>Version $currentVersion</h2>
<ul>
<li>
Update dependencies
</li>
<li>
Fix ETD errors
</li>
<li>
Update minimum version to 2026.1
</li>
<li>
Add fallback for finding rg with wsl2
Add file recency scoring
<ul>
<li>
LRU cache for file paths
</li>
<li>
Scoring is based on the recency of the file access and the frequency of the file access
</li>
</ul>
</li>
</ul>
""".trimIndent()
Expand Down
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String, FileUsageStats> =
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
Expand Down Expand Up @@ -100,7 +108,9 @@ abstract class FilesystemAction : FuzzyAction() {
globalState.matchWeightSingleChar,
globalState.matchWeightStreakModifier,
globalState.matchWeightPartialPath,
globalState.matchWeightFilename
globalState.matchWeightFilename,
globalState.matchWeightFrequency,
globalState.matchWeightRecency
)

coroutineScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br><br>
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.<br><br>
Recency boost is based on how recently a file has been accessed.
""".trimIndent(),
false
)

/////////////////////////////////////////////////////////////////
// Test bench
/////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -362,6 +380,8 @@ class FuzzierGlobalSettingsComponent(
.addComponent(matchWeightPartialPath)
.addComponent(matchWeightStreakModifier)
.addComponent(matchWeightFilename)
.addComponent(matchWeightFrequency)
.addComponent(matchWeightRecency)

.addSeparator()
.addComponent(JBLabel("<html><h2>Test bench</h2></html>"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<FuzzierSettingsService>().state.modules
combinedExclusions, project.service<FuzzierSettingsService>().state.modules, fileUsageMap
)

val iterationEntries = withContext(Dispatchers.Default) {
Expand All @@ -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<Array<Any>> = sortedList.map {
arrayOf(
(it as FuzzyMatchContainer).filename as Any,
Expand All @@ -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()
Expand Down Expand Up @@ -234,8 +240,7 @@ class TestBenchComponent : JPanel(), Disposable {
val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
val processedFiles = ConcurrentHashMap.newKeySet<String>()
val priorityQueue = PriorityQueue(
fileListLimit + 1,
compareBy<FuzzyMatchContainer> { it.getScore(prioritizeShorterDirPaths) })
fileListLimit + 1, compareBy<FuzzyMatchContainer> { it.getScore(prioritizeShorterDirPaths) })

val queueLock = Any()
var minimumScore: Int? = null
Expand All @@ -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)
Expand Down
44 changes: 44 additions & 0 deletions src/main/kotlin/com/mituuz/fuzzier/entities/FileAccessData.kt
Original file line number Diff line number Diff line change
@@ -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,
)
40 changes: 15 additions & 25 deletions src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -111,10 +110,12 @@ class FuzzyMatchContainer(
var multiMatchScore = 0
var partialPathScore = 0
var filenameScore = 0
var frequencyScore = 0
var recencyScore = 0
val highlightCharacters: MutableSet<Int> = HashSet()

fun getTotalScore(): Int {
return streakScore + multiMatchScore + partialPathScore + filenameScore
return streakScore + multiMatchScore + partialPathScore + filenameScore + frequencyScore + recencyScore
}
}

Expand All @@ -137,26 +138,15 @@ class FuzzyMatchContainer(
serialized.moduleBasePath = container.basePath
return serialized
}

fun fromListModel(listModel: DefaultListModel<FuzzyMatchContainer>): DefaultListModel<SerializedMatchContainer> {
val serializedList = DefaultListModel<SerializedMatchContainer>()
for (i in 0 until listModel.size) {
serializedList.addElement(fromFuzzyMatchContainer(listModel[i]))
}
return serializedList
}

fun toListModel(serializedList: DefaultListModel<SerializedMatchContainer>): DefaultListModel<FuzzyMatchContainer> {
val listModel = DefaultListModel<FuzzyMatchContainer>()
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
Expand All @@ -177,21 +167,21 @@ class FuzzyMatchContainer(
*
* @see FuzzierSettingsService
*/
class SerializedMatchContainerConverter : Converter<DefaultListModel<SerializedMatchContainer>>() {
override fun fromString(value: String): DefaultListModel<SerializedMatchContainer> {
class SerializedMatchContainerConverter : Converter<List<SerializedMatchContainer>>() {
override fun fromString(value: String): List<SerializedMatchContainer> {
// 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<SerializedMatchContainer> }
return ObjectInputStream(byteArrayInputStream).use { it.readObject() as List<SerializedMatchContainer> }
} catch (_: Exception) {
return DefaultListModel<SerializedMatchContainer>()
return listOf()
}
}

override fun toString(value: DefaultListModel<SerializedMatchContainer>): String {
override fun toString(value: List<SerializedMatchContainer>): String {
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(value) }
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
24 changes: 22 additions & 2 deletions src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, FileUsageStats>
) {
private val lowerSearchString: String = searchString.lowercase()
private val searchStringParts = lowerSearchString.split(" ")
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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])
}
}
Expand Down
Loading
Loading