Skip to content

Commit

Permalink
Implement a test bench to settings menu (#24)
Browse files Browse the repository at this point in the history
* Add initial bench

* Get settings from the settings component for test bench

* Fix debounce timer

* Remove unnecessary comment

* Set minimum size for test bench

* Separate content iterator for test bench

* Fix tests to use string evaluator

* Remove unnecessary variables

* Empty list when search field is empty

* Add visible score to test bench

* Add change notes

* Add a tooltip to the test bench

* Update version to 0.13

* Update change notes

---------

Co-authored-by: Mitja Leino <[email protected]>
  • Loading branch information
MituuZ and Mitja Leino authored Feb 11, 2024
1 parent 49ef606 commit 5ec6950
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 241 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = "com.mituuz"
version = "0.12"
version = "0.13"

repositories {
mavenCentral()
Expand Down
161 changes: 5 additions & 156 deletions src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ContentIterator
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
Expand All @@ -26,6 +25,7 @@ import com.intellij.openapi.util.DimensionService
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.wm.WindowManager
import com.mituuz.fuzzier.StringEvaluator.FuzzyMatchContainer
import com.mituuz.fuzzier.settings.FuzzierSettingsService
import org.apache.commons.lang3.StringUtils
import java.awt.event.*
Expand All @@ -46,17 +46,8 @@ class Fuzzier : AnAction() {
private val fuzzyDimensionKey: String = "FuzzySearchPopup"
@Volatile
var currentTask: Future<*>? = null
private var multiMatch = false

private var matchWeightPartialPath = 10
private var matchWeightSingleChar = 5
private var matchWeightStreakModifier = 10

override fun actionPerformed(p0: AnActionEvent) {
multiMatch = fuzzierSettingsService.state.multiMatch
matchWeightPartialPath = fuzzierSettingsService.state.matchWeightPartialPath
matchWeightSingleChar = fuzzierSettingsService.state.matchWeightSingleChar
matchWeightStreakModifier = fuzzierSettingsService.state.matchWeightStreakModifier
setCustomHandlers()
SwingUtilities.invokeLater {
defaultDoc = EditorFactory.getInstance().createDocument("")
Expand Down Expand Up @@ -143,14 +134,16 @@ class Fuzzier : AnAction() {
}

currentTask?.takeIf { !it.isDone }?.cancel(true)

currentTask = ApplicationManager.getApplication().executeOnPooledThread {
component.fileList.setPaintBusy(true)
val listModel = DefaultListModel<FuzzyMatchContainer>()
val projectFileIndex = ProjectFileIndex.getInstance(project)
val projectBasePath = project.basePath
val stringEvaluator = StringEvaluator(fuzzierSettingsService.state.multiMatch, fuzzierSettingsService.state.exclusionList,
fuzzierSettingsService.state.matchWeightSingleChar, fuzzierSettingsService.state.matchWeightStreakModifier,
fuzzierSettingsService.state.matchWeightPartialPath)

val contentIterator = projectBasePath?.let { getContentIterator(it, searchString, listModel) }
val contentIterator = projectBasePath?.let { stringEvaluator.getContentIterator(it, searchString, listModel) }

if (contentIterator != null) {
projectFileIndex.iterateContent(contentIterator)
Expand All @@ -169,143 +162,6 @@ class Fuzzier : AnAction() {
}
}

fun getContentIterator(projectBasePath: String, searchString: String, listModel: DefaultListModel<FuzzyMatchContainer>): ContentIterator {
return ContentIterator { file: VirtualFile ->
if (!file.isDirectory) {
val filePath = projectBasePath.let { it1 -> file.path.removePrefix(it1) }
if (isExcluded(filePath)) {
return@ContentIterator true
}
if (filePath.isNotBlank()) {
val fuzzyMatchContainer = fuzzyContainsCaseInsensitive(filePath, searchString)
if (fuzzyMatchContainer != null) {
listModel.addElement(fuzzyMatchContainer)
}
}
}
true
}
}

private fun isExcluded(filePath: String): Boolean {
val exclusionList = fuzzierSettingsService.state.exclusionList
for (e in exclusionList) {
when {
e.startsWith("*") -> {
if (filePath.endsWith(e.substring(1))) {
return true
}
}
e.endsWith("*") -> {
if (filePath.startsWith(e.substring(0, e.length - 1))) {
return true
}
}
filePath.contains(e) -> {
return true
}
}
}
return false
}

fun fuzzyContainsCaseInsensitive(filePath: String, searchString: String): FuzzyMatchContainer? {
if (searchString.isBlank()) {
return FuzzyMatchContainer(0, filePath)
}
if (searchString.length > filePath.length) {
return null
}

val lowerFilePath: String = filePath.lowercase()
val lowerSearchString: String = searchString.lowercase()
return getFuzzyMatch(lowerFilePath, lowerSearchString, filePath)
}

private fun getFuzzyMatch(lowerFilePath: String, lowerSearchString: String, filePath: String): FuzzyMatchContainer? {
var score = 0
for (s in StringUtils.split(lowerSearchString, " ")) {
score += processSearchString(s, lowerFilePath) ?: return null
}
return FuzzyMatchContainer(score, filePath)
}

private fun processSearchString(s: String, lowerFilePath: String): Int? {
var longestStreak = 0
var streak = 0
var score = 0.0
var prevIndex = -10
var match = 0
for (searchStringIndex in s.indices) {
if (lowerFilePath.length - searchStringIndex < s.length - searchStringIndex) {
return null
}

var found = -1
// Always process the whole file path for each character, assuming they're found
for (filePathIndex in lowerFilePath.indices) {
if (s[searchStringIndex] == lowerFilePath[filePathIndex]) {
match++
// Always increase score when finding a match
if (multiMatch) {
score += matchWeightSingleChar / 10.0
}
// Only check streak and update the found variable, if the current match index is greater than the previous
if (found == -1 && filePathIndex > prevIndex) {
// TODO: Does not work quite correct when handling a search string where a char is found first and then again for a multi match
// If the index is one greater than the previous chars, increment streak and update the longest streak
if (prevIndex + 1 == filePathIndex) {
streak++
if (streak > longestStreak) {
longestStreak = streak
}
} else {
streak = 1
}
// Save the first found index of a new character
prevIndex = filePathIndex
if (!multiMatch) {
// Set found to verify a match and exit the loop
found = filePathIndex
continue;
}
}
// When multiMatch is disabled, setting found exits the loop. Only set found for multiMatch
if (multiMatch) {
found = filePathIndex
}
}
}

// Check that the character was found and that it was found after the previous characters index
// Here we could skip once to broaden the search
if (found == -1 || prevIndex > found) {
return null
}
}

// If we get to here, all characters were found and have been accounted for in the score
return calculateScore(streak, longestStreak, lowerFilePath, s, score)
}

private fun calculateScore(streak: Int, longestStreak: Int, lowerFilePath: String, lowerSearchString: String, stringComparisonScore: Double): Int {
var score: Double = if (streak > longestStreak) {
(matchWeightStreakModifier / 10.0) * streak + stringComparisonScore
} else {
(matchWeightStreakModifier / 10.0) * longestStreak + stringComparisonScore
}

StringUtils.split(lowerFilePath, "/.").forEach {
if (it == lowerSearchString) {
score += matchWeightPartialPath
}
}

return score.toInt()
}

data class FuzzyMatchContainer(val score: Int, val string: String)

private fun openFile(project: Project, virtualFile: VirtualFile) {
val fileEditorManager = FileEditorManager.getInstance(project)
val currentEditor = fileEditorManager.selectedTextEditor
Expand Down Expand Up @@ -407,11 +263,4 @@ class Fuzzier : AnAction() {
}
})
}

fun setSettings() {
multiMatch = fuzzierSettingsService.state.multiMatch
matchWeightPartialPath = fuzzierSettingsService.state.matchWeightPartialPath
matchWeightSingleChar = fuzzierSettingsService.state.matchWeightSingleChar
matchWeightStreakModifier = fuzzierSettingsService.state.matchWeightStreakModifier
}
}
155 changes: 155 additions & 0 deletions src/main/kotlin/com/mituuz/fuzzier/StringEvaluator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package com.mituuz.fuzzier

import com.intellij.openapi.roots.ContentIterator
import com.intellij.openapi.vfs.VirtualFile
import org.apache.commons.lang3.StringUtils
import javax.swing.DefaultListModel

class StringEvaluator(
private var multiMatch: Boolean, private var exclusionList: List<String>, private var matchWeightSingleChar: Int,
private var matchWeightStreakModifier: Int, private var matchWeightPartialPath: Int) {

fun getContentIterator(projectBasePath: String, searchString: String, listModel: DefaultListModel<FuzzyMatchContainer>): ContentIterator {
return ContentIterator { file: VirtualFile ->
if (!file.isDirectory) {
val filePath = projectBasePath.let { it1 -> file.path.removePrefix(it1) }
if (isExcluded(filePath)) {
return@ContentIterator true
}
if (filePath.isNotBlank()) {
val fuzzyMatchContainer = fuzzyContainsCaseInsensitive(filePath, searchString)
if (fuzzyMatchContainer != null) {
listModel.addElement(fuzzyMatchContainer)
}
}
}
true
}
}

private fun isExcluded(filePath: String): Boolean {
for (e in exclusionList) {
when {
e.startsWith("*") -> {
if (filePath.endsWith(e.substring(1))) {
return true
}
}
e.endsWith("*") -> {
if (filePath.startsWith(e.substring(0, e.length - 1))) {
return true
}
}
filePath.contains(e) -> {
return true
}
}
}
return false
}

fun fuzzyContainsCaseInsensitive(filePath: String, searchString: String): FuzzyMatchContainer? {
if (searchString.isBlank()) {
return FuzzyMatchContainer(0, filePath)
}
if (searchString.length > filePath.length) {
return null
}

val lowerFilePath: String = filePath.lowercase()
val lowerSearchString: String = searchString.lowercase()
return getFuzzyMatch(lowerFilePath, lowerSearchString, filePath)
}

private fun getFuzzyMatch(lowerFilePath: String, lowerSearchString: String, filePath: String): FuzzyMatchContainer? {
var score = 0
for (s in StringUtils.split(lowerSearchString, " ")) {
score += processSearchString(s, lowerFilePath) ?: return null
}
return FuzzyMatchContainer(score, filePath)
}

private fun processSearchString(s: String, lowerFilePath: String): Int? {
var longestStreak = 0
var streak = 0
var score = 0.0
var prevIndex = -10
var match = 0
for (searchStringIndex in s.indices) {
if (lowerFilePath.length - searchStringIndex < s.length - searchStringIndex) {
return null
}

var found = -1
// Always process the whole file path for each character, assuming they're found
for (filePathIndex in lowerFilePath.indices) {
if (s[searchStringIndex] == lowerFilePath[filePathIndex]) {
match++
// Always increase score when finding a match
if (multiMatch) {
score += matchWeightSingleChar / 10.0
}
// Only check streak and update the found variable, if the current match index is greater than the previous
if (found == -1 && filePathIndex > prevIndex) {
// TODO: Does not work quite correct when handling a search string where a char is found first and then again for a multi match
// If the index is one greater than the previous chars, increment streak and update the longest streak
if (prevIndex + 1 == filePathIndex) {
streak++
if (streak > longestStreak) {
longestStreak = streak
}
} else {
streak = 1
}
// Save the first found index of a new character
prevIndex = filePathIndex
if (!multiMatch) {
// Set found to verify a match and exit the loop
found = filePathIndex
continue;
}
}
// When multiMatch is disabled, setting found exits the loop. Only set found for multiMatch
if (multiMatch) {
found = filePathIndex
}
}
}

// Check that the character was found and that it was found after the previous characters index
// Here we could skip once to broaden the search
if (found == -1 || prevIndex > found) {
return null
}
}

// If we get to here, all characters were found and have been accounted for in the score
return calculateScore(streak, longestStreak, lowerFilePath, s, score)
}

private fun calculateScore(streak: Int, longestStreak: Int, lowerFilePath: String, lowerSearchString: String, stringComparisonScore: Double): Int {
var score: Double = if (streak > longestStreak) {
(matchWeightStreakModifier / 10.0) * streak + stringComparisonScore
} else {
(matchWeightStreakModifier / 10.0) * longestStreak + stringComparisonScore
}

StringUtils.split(lowerFilePath, "/.").forEach {
if (it == lowerSearchString) {
score += matchWeightPartialPath
}
}

return score.toInt()
}

data class FuzzyMatchContainer(val score: Int, val string: String)

fun setSettings(multiMatch: Boolean, matchWeightSingleChar: Int, matchWeightPartialPath: Int,
matchWeightStreakModifier: Int) {
this.multiMatch = multiMatch
this.matchWeightPartialPath = matchWeightPartialPath
this.matchWeightSingleChar = matchWeightSingleChar
this.matchWeightStreakModifier = matchWeightStreakModifier
}
}
Loading

0 comments on commit 5ec6950

Please sign in to comment.