diff --git a/build.gradle.kts b/build.gradle.kts
index 9bc2fa3e..b3fbcaed 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 = "1.14.0"
+val currentVersion = "1.15.0"
val myGroup = "com.mituuz"
version = currentVersion
group = myGroup
@@ -40,14 +40,13 @@ intellijPlatform {
changeNotes = """
Version $currentVersion
- - Add a global exclusion list for convenience when working with multiple projects
+ - Refactor file search to use coroutines
- - Combined with the project exclusions at runtime
-
- - Add an option to use the editor font on the file list
-
- - Defaults to true
+ - Handle list size limiting during processing instead of doing them separately
+ - Add debouncing for fuzzy file preview using `SingleAlarm`
+ - Refactor everything
+ - Add auto sizing option for the popup (default)
""".trimIndent()
diff --git a/changelog.md b/changelog.md
index d1288d15..166cd6d0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
# Changelog
+## Version 1.15.0
+
+- Refactor file search to use coroutines
+ - Handle list size limiting during processing instead of doing them separately
+- Add debouncing for fuzzy file preview using `SingleAlarm`
+- Refactor everything
+- Add auto sizing option for the popup (default)
+
## Version 1.14.0
- Add a global exclusion list for convenience when working with multiple projects
diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt
index e6ade29c..4253464d 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt
@@ -21,6 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
+
package com.mituuz.fuzzier
import com.intellij.openapi.actionSystem.AnActionEvent
@@ -28,159 +29,70 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.impl.EditorHistoryManager
-import com.intellij.openapi.module.ModuleManager
-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.project.rootManager
-import com.intellij.openapi.roots.ProjectFileIndex
-import com.intellij.openapi.ui.popup.JBPopup
-import com.intellij.openapi.ui.popup.JBPopupListener
-import com.intellij.openapi.ui.popup.LightweightWindowEvent
-import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.util.SingleAlarm
+import com.mituuz.fuzzier.actions.filesystem.FilesystemAction
import com.mituuz.fuzzier.components.FuzzyFinderComponent
import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.entities.StringEvaluator
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.*
-import com.mituuz.fuzzier.util.FuzzierUtil
+import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
+import com.mituuz.fuzzier.ui.bindings.ActivationBindings
+import com.mituuz.fuzzier.ui.popup.PopupConfig
import com.mituuz.fuzzier.util.InitialViewHandler
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.apache.commons.lang3.StringUtils
-import java.awt.event.ActionEvent
-import java.awt.event.KeyEvent
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Future
-import javax.swing.AbstractAction
import javax.swing.DefaultListModel
-import javax.swing.JComponent
-import javax.swing.KeyStroke
-import kotlin.coroutines.cancellation.CancellationException
-open class Fuzzier : FuzzyAction() {
- override var popupTitle = "Fuzzy Search"
- override var dimensionKey = "FuzzySearchPopup"
+open class Fuzzier : FilesystemAction() {
+ private var previewAlarm: SingleAlarm? = null
+ private var lastPreviewKey: String? = null
+ protected open var popupTitle = "Fuzzy Search"
- // Used by FuzzierVCS to check if files are tracked by the VCS
- protected var changeListManager: ChangeListManager? = null
+ override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean =
+ { vf -> !vf.isDirectory }
override fun runAction(project: Project, actionEvent: AnActionEvent) {
- setCustomHandlers()
-
ApplicationManager.getApplication().invokeLater {
defaultDoc = EditorFactory.getInstance().createDocument("")
component = FuzzyFinderComponent(project)
+ previewAlarm = getPreviewAlarm()
createListeners(project)
- showPopup(project)
+ val maybePopup = getPopupProvider().show(
+ project = project,
+ content = component,
+ focus = component.searchField,
+ config = PopupConfig(
+ title = popupTitle,
+ preferredSizeProvider = component.preferredSize,
+ dimensionKey = "FuzzySearchPopup",
+ resetWindow = { globalState.resetWindow },
+ clearResetWindowFlag = { globalState.resetWindow = false }
+ ),
+ cleanupFunction = { cleanupPopup() }
+ )
+
+ if (maybePopup == null) return@invokeLater
+ popup = maybePopup
+
createSharedListeners(project)
(component as FuzzyFinderComponent).splitPane.dividerLocation =
globalState.splitPosition
- if (globalState.recentFilesMode != NONE) {
+ if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) {
createInitialView(project)
}
}
}
- override fun createPopup(screenDimensionKey: String): JBPopup {
- val popup = getInitialPopup(screenDimensionKey)
-
- popup.addListener(object : JBPopupListener {
- override fun onClosed(event: LightweightWindowEvent) {
- globalState.splitPosition =
- (component as FuzzyFinderComponent).splitPane.dividerLocation
- resetOriginalHandlers()
- super.onClosed(event)
- }
- })
-
- return popup
- }
-
- /**
- * Populates the file list with recently opened files
- */
- private fun createInitialView(project: Project) {
- ApplicationManager.getApplication().executeOnPooledThread {
- val editorHistoryManager = EditorHistoryManager.getInstance(project)
-
- val listModel = when (globalState.recentFilesMode) {
- RECENT_PROJECT_FILES -> InitialViewHandler.getRecentProjectFiles(
- globalState,
- fuzzierUtil,
- editorHistoryManager,
- project
- )
-
- RECENTLY_SEARCHED_FILES -> InitialViewHandler.getRecentlySearchedFiles(projectState)
- else -> {
- DefaultListModel()
- }
- }
-
- ApplicationManager.getApplication().invokeLater {
- component.fileList.model = listModel
- component.fileList.cellRenderer = getCellRenderer()
- component.fileList.setPaintBusy(false)
- if (!component.fileList.isEmpty) {
- component.fileList.setSelectedValue(listModel[0], true)
- }
- }
- }
- }
-
- override fun updateListContents(project: Project, searchString: String) {
- if (StringUtils.isBlank(searchString)) {
- handleEmptySearchString(project)
- return
- }
-
- currentTask?.takeIf { !it.isDone }?.cancel(true)
- currentTask = ApplicationManager.getApplication().executeOnPooledThread {
- try {
- // Create a reference to the current task to check if it has been cancelled
- val task = currentTask
- component.fileList.setPaintBusy(true)
- var listModel = DefaultListModel()
-
- val stringEvaluator = getStringEvaluator()
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- process(project, stringEvaluator, searchString, listModel, task)
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- listModel = fuzzierUtil.sortAndLimit(listModel)
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- ApplicationManager.getApplication().invokeLater {
- component.fileList.model = listModel
- component.fileList.cellRenderer = getCellRenderer()
- component.fileList.setPaintBusy(false)
- if (!component.fileList.isEmpty) {
- component.fileList.setSelectedValue(listModel[0], true)
- }
- }
- } catch (_: InterruptedException) {
- return@executeOnPooledThread
- } catch (_: CancellationException) {
- return@executeOnPooledThread
- }
- }
+ override fun onPopupClosed() {
+ globalState.splitPosition =
+ (component as FuzzyFinderComponent).splitPane.dividerLocation
+ previewAlarm?.dispose()
+ lastPreviewKey = null
}
- private fun handleEmptySearchString(project: Project) {
- if (globalState.recentFilesMode != NONE) {
+ override fun handleEmptySearchString(project: Project) {
+ if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) {
createInitialView(project)
} else {
ApplicationManager.getApplication().invokeLater {
@@ -190,70 +102,59 @@ open class Fuzzier : FuzzyAction() {
}
}
- private fun getStringEvaluator(): StringEvaluator {
- val combinedExclusions = buildSet {
- addAll(projectState.exclusionSet)
- addAll(globalState.globalExclusionSet)
- }
- return StringEvaluator(
- combinedExclusions,
- projectState.modules,
- changeListManager
- )
- }
-
- private fun process(
- project: Project, stringEvaluator: StringEvaluator, searchString: String,
- listModel: DefaultListModel, task: Future<*>?
- ) {
- val moduleManager = ModuleManager.getInstance(project)
- if (projectState.isProject) {
- processProject(project, stringEvaluator, searchString, listModel, task)
- } else {
- processModules(moduleManager, stringEvaluator, searchString, listModel, task)
+ private fun createListeners(project: Project) {
+ // Add a listener that updates the contents of the preview pane
+ component.fileList.addListSelectionListener { event ->
+ if (event.valueIsAdjusting) {
+ return@addListSelectionListener
+ } else {
+ previewAlarm?.cancelAndRequest()
+ }
}
- }
- private fun processProject(
- project: Project, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel, task: Future<*>?
- ) {
- val filesToIterate = ConcurrentHashMap.newKeySet()
- FuzzierUtil.fileIndexToIterationFile(filesToIterate, ProjectFileIndex.getInstance(project), project.name, task)
- processFiles(filesToIterate, stringEvaluator, listModel, searchString, task)
+ ActivationBindings.install(
+ component,
+ onActivate = { handleInput(project) }
+ )
}
- private fun processModules(
- moduleManager: ModuleManager, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel, task: Future<*>?
- ) {
- val filesToIterate = ConcurrentHashMap.newKeySet()
- for (module in moduleManager.modules) {
- FuzzierUtil.fileIndexToIterationFile(filesToIterate, module.rootManager.fileIndex, module.name, task)
+ private fun handleInput(project: Project) {
+ val selectedValue = component.fileList.selectedValue
+ val virtualFile =
+ VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
+ virtualFile?.let {
+ openFile(project, selectedValue, it)
}
- processFiles(filesToIterate, stringEvaluator, listModel, searchString, task)
}
- /**
- * Processes a set of IterationFiles concurrently
- */
- private fun processFiles(
- filesToIterate: ConcurrentHashMap.KeySetView,
- stringEvaluator: StringEvaluator, listModel: DefaultListModel,
- searchString: String, task: Future<*>?
- ) {
- val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
- val processedFiles = ConcurrentHashMap.newKeySet()
- runBlocking {
- withContext(Dispatchers.IO) {
- filesToIterate.forEach { iterationFile ->
- if (task?.isCancelled == true) return@forEach
- if (processedFiles.add(iterationFile.file.path)) {
- launch {
- stringEvaluator.evaluateFile(iterationFile, listModel, ss)
- }
+ private fun createInitialView(project: Project) {
+ component.fileList.setPaintBusy(true)
+ ApplicationManager.getApplication().executeOnPooledThread {
+ try {
+ val editorHistoryManager = EditorHistoryManager.getInstance(project)
+
+ val listModel = when (globalState.recentFilesMode) {
+ FuzzierGlobalSettingsService.RecentFilesMode.RECENT_PROJECT_FILES -> InitialViewHandler.Companion.getRecentProjectFiles(
+ globalState,
+ fuzzierUtil,
+ editorHistoryManager,
+ project
+ )
+
+ FuzzierGlobalSettingsService.RecentFilesMode.RECENTLY_SEARCHED_FILES -> InitialViewHandler.Companion.getRecentlySearchedFiles(
+ projectState
+ )
+
+ else -> {
+ DefaultListModel()
}
}
+
+ ApplicationManager.getApplication().invokeLater {
+ component.refreshModel(listModel, getCellRenderer())
+ }
+ } finally {
+ component.fileList.setPaintBusy(false)
}
}
}
@@ -281,59 +182,26 @@ open class Fuzzier : FuzzyAction() {
popup.cancel()
}
- private fun createListeners(project: Project) {
- // Add a listener that updates the contents of the preview pane
- component.fileList.addListSelectionListener { event ->
- if (!event.valueIsAdjusting) {
- if (component.fileList.isEmpty) {
- ApplicationManager.getApplication().invokeLater {
- defaultDoc?.let { (component as FuzzyFinderComponent).previewPane.updateFile(it) }
- }
- return@addListSelectionListener
- }
- val selectedValue = component.fileList.selectedValue
- val fileUrl = "file://${selectedValue?.getFileUri()}"
+ private fun getPreviewAlarm(): SingleAlarm {
+ return SingleAlarm(
+ {
+ val fuzzyFinderComponent = (component as FuzzyFinderComponent)
+ val selected = component.fileList.selectedValue
- ProgressManager.getInstance().run(object : Task.Backgroundable(null, "Loading file", false) {
- override fun run(indicator: ProgressIndicator) {
- val file = VirtualFileManager.getInstance().findFileByUrl(fileUrl)
- file?.let {
- (component as FuzzyFinderComponent).previewPane.updateFile(file)
- }
- }
- })
- }
- }
-
- // Add a mouse listener for double-click
- component.fileList.addMouseListener(object : MouseAdapter() {
- override fun mouseClicked(e: MouseEvent) {
- if (e.clickCount == 2) {
- val selectedValue = component.fileList.selectedValue
- val virtualFile =
- VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
- // Open the file in the editor
- virtualFile?.let {
- openFile(project, selectedValue, it)
- }
+ if (selected == null || component.fileList.isEmpty) {
+ defaultDoc?.let { fuzzyFinderComponent.previewPane.updateFile(it) }
+ lastPreviewKey = null
+ return@SingleAlarm
}
- }
- })
- // Add a listener that opens the currently selected file when pressing enter (focus on the text box)
- val enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
- val enterActionKey = "openFile"
- val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
- inputMap.put(enterKeyStroke, enterActionKey)
- component.searchField.actionMap.put(enterActionKey, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- val selectedValue = component.fileList.selectedValue
- val virtualFile =
- VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
- virtualFile?.let {
- openFile(project, selectedValue, it)
- }
- }
- })
+ val fileUrl = "file://${selected.getFileUri()}"
+ if (fileUrl == lastPreviewKey) return@SingleAlarm
+ lastPreviewKey = fileUrl
+
+ val vf = VirtualFileManager.getInstance().findFileByUrl(fileUrl)
+ if (vf != null) fuzzyFinderComponent.previewPane.updateFile(vf)
+ },
+ 75,
+ )
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt
index 5ee15ee7..a351cc87 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt
@@ -1,38 +1,41 @@
/*
-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.
+ * 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.
+ */
-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
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.changes.ChangeListManager
+import com.intellij.openapi.vfs.VirtualFile
/**
* Search for only VCS tracked files
*/
class FuzzierVCS : Fuzzier() {
override var popupTitle: String = "Fuzzy Search (Only VCS Tracked Files)"
- override fun updateListContents(project: Project, searchString: String) {
- changeListManager = ChangeListManager.getInstance(project)
- super.updateListContents(project, searchString)
+
+ override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean {
+ val clm = ChangeListManager.getInstance(project)
+ return { vf -> !vf.isDirectory && !clm.isIgnoredFile(vf) }
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt
index c9cc3a34..1ba9c737 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt
@@ -39,29 +39,23 @@ import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
-import com.intellij.openapi.ui.popup.JBPopup
-import com.intellij.openapi.ui.popup.JBPopupListener
-import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.mituuz.fuzzier.FuzzyGrep.Companion.MAX_NUMBER_OR_RESULTS
import com.mituuz.fuzzier.FuzzyGrep.Companion.MAX_OUTPUT_SIZE
+import com.mituuz.fuzzier.actions.FuzzyAction
import com.mituuz.fuzzier.components.FuzzyFinderComponent
import com.mituuz.fuzzier.entities.FuzzyContainer
import com.mituuz.fuzzier.entities.RowContainer
+import com.mituuz.fuzzier.ui.bindings.ActivationBindings
+import com.mituuz.fuzzier.ui.popup.PopupConfig
import kotlinx.coroutines.*
import org.apache.commons.lang3.StringUtils
-import java.awt.event.ActionEvent
-import java.awt.event.KeyEvent
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import javax.swing.AbstractAction
import javax.swing.DefaultListModel
-import javax.swing.JComponent
-import javax.swing.KeyStroke
+import javax.swing.ListModel
-open class FuzzyGrep() : FuzzyAction() {
+open class FuzzyGrep : FuzzyAction() {
companion object {
const val FUZZIER_NOTIFICATION_GROUP: String = "Fuzzier Notification Group"
@@ -72,23 +66,19 @@ open class FuzzyGrep() : FuzzyAction() {
const val MAX_NUMBER_OR_RESULTS = 1000
}
- override var popupTitle: String = "Fuzzy Grep"
- override var dimensionKey = "FuzzyGrepPopup"
var useRg = true
val isWindows = System.getProperty("os.name").lowercase().contains("win")
private var currentLaunchJob: Job? = null
- private var currentUpdateListContentJob: Job? = null
- private var actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ protected open lateinit var popupTitle: String
override fun runAction(
project: Project,
actionEvent: AnActionEvent
) {
currentLaunchJob?.cancel()
- setCustomHandlers()
val projectBasePath = project.basePath.toString()
- currentLaunchJob = actionScope.launch(Dispatchers.EDT) {
+ currentLaunchJob = actionScope?.launch(Dispatchers.EDT) {
val currentJob = currentLaunchJob
if (!isInstalled("rg", projectBasePath)) {
@@ -136,7 +126,23 @@ open class FuzzyGrep() : FuzzyAction() {
defaultDoc = EditorFactory.getInstance().createDocument("")
component = FuzzyFinderComponent(project, showSecondaryField = useRg)
createListeners(project)
- showPopup(project)
+ val maybePopup = getPopupProvider().show(
+ project = project,
+ content = component,
+ focus = component.searchField,
+ config = PopupConfig(
+ title = popupTitle,
+ preferredSizeProvider = component.preferredSize,
+ dimensionKey = "FuzzyGrepPopup",
+ resetWindow = { globalState.resetWindow },
+ clearResetWindowFlag = { globalState.resetWindow = false }
+ ),
+ cleanupFunction = { cleanupPopup() },
+ )
+
+ if (maybePopup == null) return@launch
+ popup = maybePopup
+
createSharedListeners(project)
(component as FuzzyFinderComponent).splitPane.dividerLocation =
@@ -159,21 +165,12 @@ open class FuzzyGrep() : FuzzyAction() {
Notifications.Bus.notify(grepNotification, project)
}
- override fun createPopup(screenDimensionKey: String): JBPopup {
- val popup = getInitialPopup(screenDimensionKey)
-
- popup.addListener(object : JBPopupListener {
- override fun onClosed(event: LightweightWindowEvent) {
- globalState.splitPosition =
- (component as FuzzyFinderComponent).splitPane.dividerLocation
- resetOriginalHandlers()
- super.onClosed(event)
- currentLaunchJob?.cancel()
- currentUpdateListContentJob?.cancel()
- }
- })
+ override fun onPopupClosed() {
+ globalState.splitPosition =
+ (component as FuzzyFinderComponent).splitPane.dividerLocation
- return popup
+ currentLaunchJob?.cancel()
+ currentLaunchJob = null
}
/**
@@ -199,29 +196,17 @@ open class FuzzyGrep() : FuzzyAction() {
}
currentUpdateListContentJob?.cancel()
- currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) {
- val currentJob = currentUpdateListContentJob
-
- if (currentJob?.isCancelled == true) return@launch
-
+ currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) {
component.fileList.setPaintBusy(true)
- val listModel = DefaultListModel()
-
- if (currentJob?.isCancelled == true) return@launch
-
- val results = withContext(Dispatchers.IO) {
- findInFiles(searchString, listModel, project.basePath.toString())
- listModel
- }
-
- if (currentJob?.isCancelled == true) return@launch
-
- component.fileList.model = results
- component.fileList.cellRenderer = getCellRenderer()
- if (!results.isEmpty) {
- component.fileList.selectedIndex = 0
+ try {
+ val results = withContext(Dispatchers.IO) {
+ findInFiles(searchString, project.basePath.toString())
+ }
+ coroutineContext.ensureActive()
+ component.refreshModel(results, getCellRenderer())
+ } finally {
+ component.fileList.setPaintBusy(false)
}
- component.fileList.setPaintBusy(false)
}
}
@@ -299,9 +284,10 @@ open class FuzzyGrep() : FuzzyAction() {
}
private suspend fun findInFiles(
- searchString: String, listModel: DefaultListModel,
+ searchString: String,
projectBasePath: String
- ) {
+ ): ListModel {
+ val listModel = DefaultListModel()
if (useRg) {
val secondary = (component as FuzzyFinderComponent).getSecondaryText().trim()
val commands = mutableListOf(
@@ -326,6 +312,8 @@ open class FuzzyGrep() : FuzzyAction() {
runCommand(listOf("grep", "--color=none", "-r", "-n", searchString, "."), listModel, projectBasePath)
}
}
+
+ return listModel
}
private fun createListeners(project: Project) {
@@ -333,7 +321,7 @@ open class FuzzyGrep() : FuzzyAction() {
component.fileList.addListSelectionListener { event ->
if (!event.valueIsAdjusting) {
if (component.fileList.isEmpty) {
- actionScope.launch(Dispatchers.EDT) {
+ actionScope?.launch(Dispatchers.EDT) {
defaultDoc?.let { (component as FuzzyFinderComponent).previewPane.updateFile(it) }
}
return@addListSelectionListener
@@ -341,7 +329,7 @@ open class FuzzyGrep() : FuzzyAction() {
val selectedValue = component.fileList.selectedValue
val fileUrl = "file://${selectedValue?.getFileUri()}"
- actionScope.launch(Dispatchers.Default) {
+ actionScope?.launch(Dispatchers.Default) {
val file = withContext(Dispatchers.IO) {
VirtualFileManager.getInstance().findFileByUrl(fileUrl)
}
@@ -356,36 +344,19 @@ open class FuzzyGrep() : FuzzyAction() {
}
}
- // Add a mouse listener for double-click
- component.fileList.addMouseListener(object : MouseAdapter() {
- override fun mouseClicked(e: MouseEvent) {
- if (e.clickCount == 2) {
- val selectedValue = component.fileList.selectedValue
- val virtualFile =
- VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
- // Open the file in the editor
- virtualFile?.let {
- openFile(project, selectedValue, it)
- }
- }
- }
- })
-
- // Add a listener that opens the currently selected file when pressing enter (focus on the text box)
- val enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
- val enterActionKey = "openFile"
- val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
- inputMap.put(enterKeyStroke, enterActionKey)
- component.searchField.actionMap.put(enterActionKey, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- val selectedValue = component.fileList.selectedValue
- val virtualFile =
- VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
- virtualFile?.let {
- openFile(project, selectedValue, it)
- }
- }
- })
+ ActivationBindings.install(
+ component,
+ onActivate = { handleInput(project) }
+ )
+ }
+
+ private fun handleInput(project: Project) {
+ val selectedValue = component.fileList.selectedValue
+ val virtualFile =
+ VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}")
+ virtualFile?.let {
+ openFile(project, selectedValue, it)
+ }
}
private fun openFile(project: Project, fuzzyContainer: FuzzyContainer?, virtualFile: VirtualFile) {
diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt
index cea184b7..cef42f09 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt
@@ -1,27 +1,25 @@
/*
-
- 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.
-
+ * 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
@@ -31,9 +29,12 @@ import javax.swing.DefaultListModel
class FuzzyGrepCaseInsensitive : FuzzyGrep() {
override var popupTitle: String = "Fuzzy Grep (Case Insensitive)"
- override var dimensionKey = "FuzzyGrepCaseInsensitivePopup"
- override suspend fun runCommand(commands: List, listModel: DefaultListModel, projectBasePath: String) {
+ override suspend fun runCommand(
+ commands: List,
+ listModel: DefaultListModel,
+ projectBasePath: String
+ ) {
val modifiedCommands = commands.toMutableList()
if (isWindows && !useRg) {
// Customize findstr for case insensitivity
diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt
index 7332ed2d..8dfdf0c9 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt
@@ -21,6 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
+
package com.mituuz.fuzzier
import com.intellij.notification.Notification
@@ -30,44 +31,27 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileEditorManager
-import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
-import com.intellij.openapi.project.rootManager
-import com.intellij.openapi.roots.ProjectFileIndex
-import com.intellij.openapi.ui.popup.JBPopup
-import com.intellij.openapi.ui.popup.JBPopupListener
-import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
+import com.mituuz.fuzzier.actions.filesystem.FilesystemAction
import com.mituuz.fuzzier.components.SimpleFinderComponent
-import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.entities.StringEvaluator
-import com.mituuz.fuzzier.util.FuzzierUtil
-import org.apache.commons.lang3.StringUtils
-import java.awt.event.ActionEvent
-import java.awt.event.KeyEvent
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Future
-import javax.swing.AbstractAction
+import com.mituuz.fuzzier.ui.bindings.ActivationBindings
+import com.mituuz.fuzzier.ui.popup.PopupConfig
import javax.swing.DefaultListModel
-import javax.swing.JComponent
-import javax.swing.KeyStroke
-import kotlin.coroutines.cancellation.CancellationException
-class FuzzyMover : FuzzyAction() {
- override var popupTitle = "Fuzzy File Mover"
- override var dimensionKey = "FuzzyMoverPopup"
+class FuzzyMover : FilesystemAction() {
lateinit var movableFile: PsiFile
lateinit var currentFile: VirtualFile
- override fun runAction(project: Project, actionEvent: AnActionEvent) {
- setCustomHandlers()
+ override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean {
+ return { vf -> if (component.isDirSelector) vf.isDirectory else !vf.isDirectory }
+ }
+ override fun runAction(project: Project, actionEvent: AnActionEvent) {
ApplicationManager.getApplication().invokeLater {
component = SimpleFinderComponent()
createListeners(project)
@@ -77,47 +61,38 @@ class FuzzyMover : FuzzyAction() {
component.fileList.setEmptyText("Press enter to use current file: ${currentFile.path}")
}
- showPopup(project)
+ val maybePopup = getPopupProvider().show(
+ project = project,
+ content = component,
+ focus = component.searchField,
+ config = PopupConfig(
+ title = "Fuzzy File Mover",
+ preferredSizeProvider = component.preferredSize,
+ dimensionKey = "FuzzyMoverPopup",
+ resetWindow = { globalState.resetWindow },
+ clearResetWindowFlag = { globalState.resetWindow = false }),
+ cleanupFunction = { cleanupPopup() },
+ )
+
+ if (maybePopup == null) return@invokeLater
+ popup = maybePopup
+
createSharedListeners(project)
}
}
- override fun createPopup(screenDimensionKey: String): JBPopup {
- val popup = getInitialPopup(screenDimensionKey)
-
- popup.addListener(object : JBPopupListener {
- override fun onClosed(event: LightweightWindowEvent) {
- resetOriginalHandlers()
- super.onClosed(event)
- }
- })
-
- return popup
+ override fun handleEmptySearchString(project: Project) {
+ ApplicationManager.getApplication().invokeLater {
+ component.fileList.model = DefaultListModel()
+ }
}
private fun createListeners(project: Project) {
- // Add a mouse listener for double-click
- component.fileList.addMouseListener(object : MouseAdapter() {
- override fun mouseClicked(e: MouseEvent) {
- if (e.clickCount == 2) {
- handleInput(project)
- }
- }
- })
-
- val enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
- val enterActionKey = "openFile"
- val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
- inputMap.put(enterKeyStroke, enterActionKey)
- component.searchField.actionMap.put(enterActionKey, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- handleInput(project)
- }
- })
+ ActivationBindings.install(
+ component, onActivate = { handleInput(project) })
}
- fun handleInput(project: Project): CompletableFuture {
- val completableFuture = CompletableFuture()
+ fun handleInput(project: Project) {
var selectedValue = component.fileList.selectedValue?.getFileUri()
if (selectedValue == null) {
selectedValue = currentFile.path
@@ -135,137 +110,31 @@ class FuzzyMover : FuzzyAction() {
component.isDirSelector = true
component.searchField.text = ""
component.fileList.setEmptyText("Select target folder")
- completableFuture.complete(null)
}
} else {
ApplicationManager.getApplication().invokeLater {
- val targetDirectory: CompletableFuture = CompletableFuture.supplyAsync {
- ApplicationManager.getApplication().runReadAction {
- val virtualDir =
- VirtualFileManager.getInstance().findFileByUrl("file://$selectedValue")
- virtualDir?.let { PsiManager.getInstance(project).findDirectory(it) }
- }
+ val targetDir = ApplicationManager.getApplication().runReadAction {
+ val virtualDir = VirtualFileManager.getInstance().findFileByUrl("file://$selectedValue")
+ virtualDir?.let { PsiManager.getInstance(project).findDirectory(it) }
}
- targetDirectory.thenAcceptAsync { targetDir ->
- val originalFilePath = movableFile.virtualFile.path
- if (targetDir != null) {
- WriteCommandAction.runWriteCommandAction(project) {
- movableFile.virtualFile.move(movableFile.manager, targetDir.virtualFile)
- }
- val notification = Notification(
- "Fuzzier Notification Group",
- "File moved successfully",
- "Moved $originalFilePath to $selectedValue",
- NotificationType.INFORMATION
- )
- Notifications.Bus.notify(notification, project)
- ApplicationManager.getApplication().invokeLater {
- popup.cancel()
- }
- completableFuture.complete(null)
- } else {
- completableFuture.complete(null)
+ val originalFilePath = movableFile.virtualFile.path
+ if (targetDir != null) {
+ WriteCommandAction.runWriteCommandAction(project) {
+ movableFile.virtualFile.move(movableFile.manager, targetDir.virtualFile)
}
- }
- }
- }
- return completableFuture
- }
-
- override fun updateListContents(project: Project, searchString: String) {
- if (StringUtils.isBlank(searchString)) {
- ApplicationManager.getApplication().invokeLater {
- component.fileList.model = DefaultListModel()
- }
- return
- }
-
- currentTask?.takeIf { !it.isDone }?.cancel(true)
- currentTask = ApplicationManager.getApplication().executeOnPooledThread {
- try {
- // Create a reference to the current task to check if it has been cancelled
- val task = currentTask
- component.fileList.setPaintBusy(true)
- var listModel = DefaultListModel()
-
- val stringEvaluator = getStringEvaluator()
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- process(project, stringEvaluator, searchString, listModel, task)
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- listModel = fuzzierUtil.sortAndLimit(listModel, true)
-
- if (task?.isCancelled == true) return@executeOnPooledThread
-
- ApplicationManager.getApplication().invokeLater {
- component.fileList.model = listModel
- component.fileList.cellRenderer = getCellRenderer()
- component.fileList.setPaintBusy(false)
- if (!component.fileList.isEmpty) {
- component.fileList.setSelectedValue(listModel[0], true)
+ val notification = Notification(
+ "Fuzzier Notification Group",
+ "File moved successfully",
+ "Moved $originalFilePath to $selectedValue",
+ NotificationType.INFORMATION
+ )
+ Notifications.Bus.notify(notification, project)
+ ApplicationManager.getApplication().invokeLater {
+ popup.cancel()
}
}
- } catch (_: InterruptedException) {
- return@executeOnPooledThread
- } catch (_: CancellationException) {
- return@executeOnPooledThread
- }
- }
- }
-
- private fun getStringEvaluator(): StringEvaluator {
- val combinedExclusions = buildSet {
- addAll(projectState.exclusionSet)
- addAll(globalState.globalExclusionSet)
- }
- return StringEvaluator(
- combinedExclusions,
- projectState.modules
- )
- }
-
- private fun process(
- project: Project, stringEvaluator: StringEvaluator, searchString: String,
- listModel: DefaultListModel, task: Future<*>?
- ) {
- val moduleManager = ModuleManager.getInstance(project)
- val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
- if (projectState.isProject) {
- processProject(project, stringEvaluator, ss, listModel, task)
- } else {
- processModules(moduleManager, stringEvaluator, ss, listModel, task)
- }
- }
-
- private fun processProject(
- project: Project, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel, task: Future<*>?
- ) {
- val contentIterator = if (!component.isDirSelector) {
- stringEvaluator.getContentIterator(project.name, searchString, listModel, task)
- } else {
- stringEvaluator.getDirIterator(project.name, searchString, listModel, task)
- }
- ProjectFileIndex.getInstance(project).iterateContent(contentIterator)
- }
-
- private fun processModules(
- moduleManager: ModuleManager, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel, task: Future<*>?
- ) {
- for (module in moduleManager.modules) {
- val moduleFileIndex = module.rootManager.fileIndex
-
- val contentIterator = if (!component.isDirSelector) {
- stringEvaluator.getContentIterator(module.name, searchString, listModel, task)
- } else {
- stringEvaluator.getDirIterator(module.name, searchString, listModel, task)
}
- moduleFileIndex.iterateContent(contentIterator)
}
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt
similarity index 85%
rename from src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt
rename to src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt
index 7660c1b2..482da7ad 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-package com.mituuz.fuzzier
+package com.mituuz.fuzzier.actions
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
@@ -39,29 +39,27 @@ import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
-import com.intellij.openapi.ui.popup.JBPopupFactory
-import com.intellij.openapi.util.DimensionService
-import com.intellij.openapi.wm.WindowManager
import com.mituuz.fuzzier.components.FuzzyComponent
+import com.mituuz.fuzzier.components.FuzzyFinderComponent
import com.mituuz.fuzzier.entities.FuzzyContainer
import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
+import com.mituuz.fuzzier.ui.popup.AutoSizePopupProvider
+import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider
+import com.mituuz.fuzzier.ui.popup.PopupProvider
import com.mituuz.fuzzier.util.FuzzierUtil
-import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey
+import kotlinx.coroutines.*
import java.awt.Component
import java.awt.Font
import java.awt.event.ActionEvent
import java.util.*
import java.util.Timer
import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Future
import javax.swing.*
import kotlin.concurrent.schedule
abstract class FuzzyAction : AnAction() {
- open lateinit var dimensionKey: String
- open lateinit var popupTitle: String
lateinit var component: FuzzyComponent
lateinit var popup: JBPopup
private lateinit var originalDownHandler: EditorActionHandler
@@ -72,54 +70,36 @@ abstract class FuzzyAction : AnAction() {
protected var defaultDoc: Document? = null
private val fileTypeManager = FileTypeManager.getInstance()
- @Volatile
- var currentTask: Future<*>? = null
val fuzzierUtil = FuzzierUtil()
+ protected open var currentUpdateListContentJob: Job? = null
+ protected open var actionScope: CoroutineScope? = null
+
+ protected fun getPopupProvider(): PopupProvider {
+ return when (globalState.popupSizing) {
+ FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE -> {
+ val wf = (globalState.autoWidthPercent.coerceIn(10, 100)) / 100.0
+ val hf = (globalState.autoHeightPercent.coerceIn(10, 100)) / 100.0
+ AutoSizePopupProvider(wf, hf)
+ }
+
+ FuzzierGlobalSettingsService.PopupSizing.VANILLA -> DefaultPopupProvider()
+ }
+ }
override fun actionPerformed(actionEvent: AnActionEvent) {
val project = actionEvent.project
if (project != null) {
projectState = project.service().state
fuzzierUtil.parseModules(project)
+ setCustomHandlers()
+ actionScope?.cancel()
+ actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
runAction(project, actionEvent)
}
}
abstract fun runAction(project: Project, actionEvent: AnActionEvent)
- abstract fun createPopup(screenDimensionKey: String): JBPopup
-
- fun getInitialPopup(screenDimensionKey: String): JBPopup {
- return JBPopupFactory
- .getInstance()
- .createComponentPopupBuilder(component, component.searchField)
- .setFocusable(true)
- .setRequestFocus(true)
- .setResizable(true)
- .setDimensionServiceKey(null, screenDimensionKey, true)
- .setTitle(popupTitle)
- .setMovable(true)
- .setShowBorder(true)
- .createPopup()
- }
-
- fun showPopup(project: Project) {
- val mainWindow = WindowManager.getInstance().getIdeFrame(project)?.component
- mainWindow?.let {
- val screenBounds = it.graphicsConfiguration.bounds
- val screenDimensionKey = createDimensionKey(dimensionKey, screenBounds)
-
- if (globalState.resetWindow) {
- DimensionService.getInstance().setSize(screenDimensionKey, component.preferredSize, null)
- DimensionService.getInstance().setLocation(screenDimensionKey, null, null)
- globalState.resetWindow = false
- }
-
- popup = createPopup(screenDimensionKey)
- popup.showInCenterOf(it)
- }
- }
-
fun createSharedListeners(project: Project) {
val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
@@ -168,7 +148,7 @@ abstract class FuzzyAction : AnAction() {
document.addDocumentListener(listener, popup)
// Also listen to changes in the secondary search field (if present)
- (component as? com.mituuz.fuzzier.components.FuzzyFinderComponent)?.addSecondaryDocumentListener(
+ (component as? FuzzyFinderComponent)?.addSecondaryDocumentListener(
listener,
popup
)
@@ -176,6 +156,19 @@ abstract class FuzzyAction : AnAction() {
abstract fun updateListContents(project: Project, searchString: String)
+ protected open fun onPopupClosed() = Unit
+
+ fun cleanupPopup() {
+ resetOriginalHandlers()
+
+ currentUpdateListContentJob?.cancel()
+ currentUpdateListContentJob = null
+
+ actionScope?.cancel()
+
+ onPopupClosed()
+ }
+
fun setCustomHandlers() {
val actionManager = EditorActionManager.getInstance()
originalDownHandler = actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN)
diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt
new file mode 100644
index 00000000..94ba0f6f
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt
@@ -0,0 +1,214 @@
+/*
+ * 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.actions.filesystem
+
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.rootManager
+import com.intellij.openapi.roots.ProjectFileIndex
+import com.intellij.openapi.vfs.VirtualFile
+import com.mituuz.fuzzier.actions.FuzzyAction
+import com.mituuz.fuzzier.entities.*
+import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector
+import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector
+import com.mituuz.fuzzier.util.FuzzierUtil
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import javax.swing.DefaultListModel
+
+abstract class FilesystemAction : FuzzyAction() {
+ private var collector: IterationFileCollector = IntelliJIterationFileCollector()
+
+ abstract override fun runAction(
+ project: Project,
+ actionEvent: AnActionEvent
+ )
+
+ abstract fun buildFileFilter(project: Project): (VirtualFile) -> Boolean
+
+ abstract fun handleEmptySearchString(project: Project)
+
+ suspend fun collectIterationFiles(project: Project): List {
+ val ctx = currentCoroutineContext()
+ val job = ctx.job
+
+ val indexTargets = if (projectState.isProject) {
+ listOf(ProjectFileIndex.getInstance(project) to project.name)
+ } else {
+ val moduleManager = ModuleManager.getInstance(project)
+ moduleManager.modules.map { it.rootManager.fileIndex to it.name }
+ }
+
+ return collector.collectFiles(
+ targets = indexTargets,
+ shouldContinue = { job.isActive },
+ fileFilter = buildFileFilter(project)
+ )
+ }
+
+ fun getStringEvaluator(): StringEvaluator {
+ val combinedExclusions = buildSet {
+ addAll(projectState.exclusionSet)
+ addAll(globalState.globalExclusionSet)
+ }
+ return StringEvaluator(
+ combinedExclusions,
+ projectState.modules,
+ )
+ }
+
+ /**
+ * Processes a set of IterationFiles concurrently
+ * @return a priority list which has been size limited and sorted
+ */
+ suspend fun processIterationEntries(
+ fileEntries: List,
+ stringEvaluator: StringEvaluator,
+ searchString: String,
+ fileListLimit: Int,
+ ignoredChars: String,
+ prioritizeShorterDirPaths: Boolean,
+ ): DefaultListModel {
+ val ss = FuzzierUtil.cleanSearchString(searchString, ignoredChars)
+ val processedFiles = ConcurrentHashMap.newKeySet()
+ val listLimit = fileListLimit
+ val priorityQueue = PriorityQueue(
+ listLimit + 1,
+ compareBy { it.getScore(prioritizeShorterDirPaths) }
+ )
+
+ val queueLock = Any()
+ var minimumScore: Int? = null
+
+ val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
+ val parallelism = (cores - 1).coerceIn(1, 8)
+
+ val matchConfig = MatchConfig(
+ globalState.tolerance,
+ globalState.multiMatch,
+ globalState.matchWeightSingleChar,
+ globalState.matchWeightStreakModifier,
+ globalState.matchWeightPartialPath,
+ globalState.matchWeightFilename
+ )
+
+ coroutineScope {
+ val ch = Channel(capacity = parallelism * 2)
+
+ repeat(parallelism) {
+ launch {
+ for (iterationFile in ch) {
+ val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss, matchConfig)
+ container?.let { fuzzyMatchContainer ->
+ synchronized(queueLock) {
+ minimumScore = priorityQueue.maybeAdd(
+ minimumScore,
+ fuzzyMatchContainer,
+ fileListLimit,
+ globalState.prioritizeShorterDirPaths
+ )
+ }
+ }
+ }
+ }
+ }
+
+ fileEntries
+ .filter { processedFiles.add(it.path) }
+ .forEach { ch.send(it) }
+ ch.close()
+ }
+
+
+ val result = DefaultListModel()
+ result.addAll(
+ priorityQueue.sortedWith(
+ compareByDescending { it.getScore(prioritizeShorterDirPaths) })
+ )
+ return result
+ }
+
+ private fun PriorityQueue.maybeAdd(
+ minimumScore: Int?,
+ fuzzyMatchContainer: FuzzyMatchContainer,
+ fileListLimit: Int,
+ prioritizeShorterFilePaths: Boolean,
+ ): Int? {
+ var ret = minimumScore
+
+ if (minimumScore == null || fuzzyMatchContainer.getScore(prioritizeShorterFilePaths) > minimumScore) {
+ this.add(fuzzyMatchContainer)
+ if (this.size > fileListLimit) {
+ this.remove()
+ ret = this.peek().getScore(prioritizeShorterFilePaths)
+ }
+ }
+
+ return ret
+ }
+
+ override fun updateListContents(project: Project, searchString: String) {
+ if (searchString.isEmpty()) {
+ handleEmptySearchString(project)
+ return
+ }
+
+ currentUpdateListContentJob?.cancel()
+ currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) {
+ component.fileList.setPaintBusy(true)
+
+ try {
+ val stringEvaluator = getStringEvaluator()
+ coroutineContext.ensureActive()
+
+ val iterationEntries = withContext(Dispatchers.Default) {
+ collectIterationFiles(project)
+ }
+ coroutineContext.ensureActive()
+
+ val listModel = withContext(Dispatchers.Default) {
+ processIterationEntries(
+ iterationEntries,
+ stringEvaluator,
+ searchString,
+ globalState.fileListLimit,
+ projectState.ignoredCharacters,
+ globalState.prioritizeShorterDirPaths,
+ )
+ }
+ coroutineContext.ensureActive()
+
+ component.refreshModel(listModel, getCellRenderer())
+ } finally {
+ component.fileList.setPaintBusy(false)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
index 18765f76..a9376ca1 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt
@@ -36,8 +36,7 @@ import com.intellij.ui.components.JBTextArea
import com.intellij.util.ui.FormBuilder
import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.SearchPosition
+import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.*
import java.awt.Component
import javax.swing.*
import javax.swing.border.LineBorder
@@ -166,6 +165,33 @@ class FuzzierGlobalSettingsComponent(
false
)
+ val popupSizingSelector = SettingsComponent(
+ ComboBox(), "Popup sizing",
+ """
+ Select how the popup size is determined.
+ Auto size uses content-aware sizing and ignores manual dimensions.
+ Vanilla uses the persistent DimensionService sizing with default dimensions below.
+ """.trimIndent(),
+ false
+ )
+
+ val autoSizePercentPanel = JPanel().apply {
+ layout = BoxLayout(this, BoxLayout.X_AXIS)
+ add(JBLabel("Width %: "))
+ add(JBIntSpinner(80, 10, 100))
+ add(Box.createHorizontalStrut(10))
+ add(JBLabel("Height %: "))
+ add(JBIntSpinner(80, 10, 100))
+ }
+ val autoSizePercentages = SettingsComponent(
+ autoSizePercentPanel, "Auto-size (% of window)",
+ """
+ Percentage of the IDE window used when popup sizing is set to Auto size.
+ Min: 10, Max: 100
+ """.trimIndent(),
+ false
+ )
+
val dimensionComponent = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
add(JBLabel("Width: "))
@@ -309,6 +335,8 @@ class FuzzierGlobalSettingsComponent(
.addComponent(filenameTypeSelector)
.addComponent(highlightFilename)
.addComponent(searchPosition)
+ .addComponent(popupSizingSelector)
+ .addComponent(autoSizePercentages)
.addComponent(defaultDimension)
.addComponent(previewFontSize)
.addComponent(fileListUseEditorFont)
@@ -383,6 +411,25 @@ class FuzzierGlobalSettingsComponent(
searchPosition.getSearchPositionComboBox().addItem(sp)
}
+ popupSizingSelector.getPopupSizingComboBox().renderer = object : DefaultListCellRenderer() {
+ override fun getListCellRendererComponent(
+ list: JList<*>?,
+ value: Any?,
+ index: Int,
+ isSelected: Boolean,
+ cellHasFocus: Boolean
+ ): Component {
+ val renderer =
+ super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) as JLabel
+ val sizing = value as PopupSizing
+ renderer.text = sizing.text
+ return renderer
+ }
+ }
+ for (s in PopupSizing.entries) {
+ popupSizingSelector.getPopupSizingComboBox().addItem(s)
+ }
+
filenameTypeSelector.getFilenameTypeComboBox().renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt
index fe6e39c4..71de5404 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt
@@ -28,9 +28,19 @@ import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBList
import com.mituuz.fuzzier.entities.FuzzyContainer
import javax.swing.JPanel
+import javax.swing.ListCellRenderer
+import javax.swing.ListModel
open class FuzzyComponent : JPanel() {
- var fileList = JBList()
+ var fileList = JBList()
var searchField = EditorTextField()
var isDirSelector = false
+
+ fun refreshModel(listModel: ListModel, cellRenderer: ListCellRenderer) {
+ fileList.model = listModel
+ fileList.cellRenderer = cellRenderer
+ if (!fileList.isEmpty) {
+ fileList.selectedIndex = 0
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyFinderComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyFinderComponent.kt
index b227d546..862e5a48 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyFinderComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyFinderComponent.kt
@@ -78,7 +78,7 @@ class FuzzyFinderComponent(project: Project, private val showSecondaryField: Boo
}
searchField.text = ""
val fileListScrollPane = JBScrollPane()
- fileList = JBList()
+ fileList = JBList()
fileList.selectionMode = 0
fileListScrollPane.setViewportView(fileList)
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt
index 2289ea9f..594e4367 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt
@@ -1,26 +1,26 @@
/*
-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.
-*/
+ * 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.components
import com.intellij.icons.AllIcons
@@ -33,10 +33,11 @@ import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
+import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.PopupSizing
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode
import javax.swing.JButton
-import javax.swing.JPanel
import javax.swing.JComponent
+import javax.swing.JPanel
class SettingsComponent {
val includesSeparateLabel: Boolean
@@ -98,6 +99,11 @@ class SettingsComponent {
return component as ComboBox
}
+ fun getPopupSizingComboBox(): ComboBox {
+ @Suppress("UNCHECKED_CAST")
+ return component as ComboBox
+ }
+
fun getIntSpinner(index: Int): JBIntSpinner {
return (component as JPanel).getComponent(index) as JBIntSpinner
}
diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
index 91e4336c..c98798bc 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt
@@ -33,19 +33,23 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.ProjectFileIndex
+import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.table.JBTable
import com.intellij.uiDesigner.core.GridConstraints
import com.intellij.uiDesigner.core.GridLayoutManager
-import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.entities.FuzzyMatchContainer
-import com.mituuz.fuzzier.entities.StringEvaluator
+import com.mituuz.fuzzier.entities.*
+import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector
+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
import org.apache.commons.lang3.StringUtils
import java.awt.Dimension
import java.util.*
+import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
import javax.swing.DefaultListModel
import javax.swing.JPanel
@@ -63,6 +67,9 @@ class TestBenchComponent : JPanel(), Disposable {
var currentTask: Future<*>? = null
private lateinit var liveSettingsComponent: FuzzierGlobalSettingsComponent
private lateinit var projectState: FuzzierSettingsService.State
+ private var currentUpdateListContentJob: Job? = null
+ private var actionScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ private var collector: IterationFileCollector = IntelliJIterationFileCollector()
fun fill(settingsComponent: FuzzierGlobalSettingsComponent) {
val project = ProjectManager.getInstance().openProjects[0]
@@ -77,8 +84,7 @@ class TestBenchComponent : JPanel(), Disposable {
scrollPane.setViewportView(table)
add(
- scrollPane,
- GridConstraints(
+ scrollPane, GridConstraints(
0,
0,
1,
@@ -95,8 +101,7 @@ class TestBenchComponent : JPanel(), Disposable {
)
)
add(
- searchField,
- GridConstraints(
+ searchField, GridConstraints(
1,
0,
1,
@@ -137,96 +142,59 @@ class TestBenchComponent : JPanel(), Disposable {
}
// Use live settings from the component (unsaved UI state) so changes are reflected immediately
- val liveGlobalExclusions = liveSettingsComponent.globalExclusionTextArea.text
- .lines()
- .map { it.trim() }
- .filter { it.isNotEmpty() }
- .toSet()
+ val liveGlobalExclusions =
+ liveSettingsComponent.globalExclusionTextArea.text.lines().map { it.trim() }.filter { it.isNotEmpty() }
+ .toSet()
val combinedExclusions = buildSet {
addAll(projectState.exclusionSet)
addAll(liveGlobalExclusions)
}
- val stringEvaluator = StringEvaluator(
- combinedExclusions,
- project.service().state.modules
- )
-
- currentTask?.takeIf { !it.isDone }?.cancel(true)
-
- currentTask = ApplicationManager.getApplication().executeOnPooledThread {
+ currentUpdateListContentJob?.cancel()
+ currentUpdateListContentJob = actionScope.launch {
table.setPaintBusy(true)
- val listModel = DefaultListModel()
-
- process(project, stringEvaluator, searchString, listModel)
-
- val sortedList = listModel.elements().toList().sortedByDescending { (it as FuzzyMatchContainer).getScore() }
- val data: Array> = sortedList.map {
- arrayOf(
- (it as FuzzyMatchContainer).filename as Any,
- it.filePath as Any,
- it.score.streakScore as Any,
- it.score.multiMatchScore as Any,
- it.score.partialPathScore as Any,
- it.score.filenameScore as Any,
- it.score.getTotalScore() as Any
+
+ try {
+ val stringEvaluator = StringEvaluator(
+ combinedExclusions, project.service().state.modules
)
- }.toTypedArray()
- val tableModel = DefaultTableModel(data, columnNames)
- table.model = tableModel
- table.setPaintBusy(false)
- }
- }
+ val iterationEntries = withContext(Dispatchers.Default) {
+ collectIterationFiles(project)
+ }
- private fun process(
- project: Project, stringEvaluator: StringEvaluator, searchString: String,
- listModel: DefaultListModel
- ) {
- val moduleManager = ModuleManager.getInstance(project)
- if (project.service().state.isProject) {
- processProject(project, stringEvaluator, searchString, listModel)
- } else {
- processModules(moduleManager, stringEvaluator, searchString, listModel)
- }
- }
+ val prioritizeShorterDirPaths = liveSettingsComponent.prioritizeShortDirs.getCheckBox().isSelected
+ val listModel = withContext(Dispatchers.Default) {
+ processIterationEntries(
+ iterationEntries,
+ stringEvaluator,
+ searchString,
+ liveSettingsComponent.fileListLimit.getIntSpinner().value as Int,
+ prioritizeShorterDirPaths,
+ )
+ }
- private fun processProject(
- project: Project, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel
- ) {
- val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
- val contentIterator = stringEvaluator.getContentIterator(project.name, ss, listModel, null)
-
- val scoreCalculator = stringEvaluator.scoreCalculator
- scoreCalculator.setMultiMatch(liveSettingsComponent.multiMatchActive.getCheckBox().isSelected)
- scoreCalculator.setMatchWeightSingleChar(liveSettingsComponent.matchWeightSingleChar.getIntSpinner().value as Int)
- scoreCalculator.setMatchWeightStreakModifier(liveSettingsComponent.matchWeightStreakModifier.getIntSpinner().value as Int)
- scoreCalculator.setMatchWeightPartialPath(liveSettingsComponent.matchWeightPartialPath.getIntSpinner().value as Int)
- scoreCalculator.setFilenameMatchWeight(liveSettingsComponent.matchWeightFilename.getIntSpinner().value as Int)
- scoreCalculator.setTolerance(liveSettingsComponent.tolerance.getIntSpinner().value as Int)
- ProjectFileIndex.getInstance(project).iterateContent(contentIterator)
- }
+ val sortedList =
+ listModel.elements().toList()
+ .sortedByDescending { (it as FuzzyMatchContainer).getScore(prioritizeShorterDirPaths) }
+ val data: Array> = sortedList.map {
+ arrayOf(
+ (it as FuzzyMatchContainer).filename as Any,
+ it.filePath as Any,
+ it.score.streakScore as Any,
+ it.score.multiMatchScore as Any,
+ it.score.partialPathScore as Any,
+ it.score.filenameScore as Any,
+ it.score.getTotalScore() as Any
+ )
+ }.toTypedArray()
- private fun processModules(
- moduleManager: ModuleManager, stringEvaluator: StringEvaluator,
- searchString: String, listModel: DefaultListModel
- ) {
- for (module in moduleManager.modules) {
- val moduleFileIndex = module.rootManager.fileIndex
- val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
- val contentIterator = stringEvaluator.getContentIterator(module.name, ss, listModel, null)
-
- val scoreCalculator = stringEvaluator.scoreCalculator
- scoreCalculator.setMultiMatch(liveSettingsComponent.multiMatchActive.getCheckBox().isSelected)
- scoreCalculator.setMatchWeightSingleChar(liveSettingsComponent.matchWeightSingleChar.getIntSpinner().value as Int)
- scoreCalculator.setMatchWeightStreakModifier(liveSettingsComponent.matchWeightStreakModifier.getIntSpinner().value as Int)
- scoreCalculator.setMatchWeightPartialPath(liveSettingsComponent.matchWeightPartialPath.getIntSpinner().value as Int)
- scoreCalculator.setFilenameMatchWeight(liveSettingsComponent.matchWeightFilename.getIntSpinner().value as Int)
- scoreCalculator.setTolerance(liveSettingsComponent.tolerance.getIntSpinner().value as Int)
-
- moduleFileIndex.iterateContent(contentIterator)
+ val tableModel = DefaultTableModel(data, columnNames)
+ table.model = tableModel
+ } finally {
+ table.setPaintBusy(false)
+ }
}
}
@@ -247,4 +215,100 @@ class TestBenchComponent : JPanel(), Disposable {
}
}
}
+
+ suspend fun collectIterationFiles(project: Project): List {
+ val ctx = currentCoroutineContext()
+ val job = ctx.job
+
+ val indexTargets = if (projectState.isProject) {
+ listOf(ProjectFileIndex.getInstance(project) to project.name)
+ } else {
+ val moduleManager = ModuleManager.getInstance(project)
+ moduleManager.modules.map { it.rootManager.fileIndex to it.name }
+ }
+
+ return collector.collectFiles(
+ targets = indexTargets, shouldContinue = { job.isActive }, fileFilter = buildFileFilter()
+ )
+ }
+
+ private fun buildFileFilter(): (VirtualFile) -> Boolean = { vf -> !vf.isDirectory }
+
+ suspend fun processIterationEntries(
+ fileEntries: List,
+ stringEvaluator: StringEvaluator,
+ searchString: String,
+ fileListLimit: Int,
+ prioritizeShorterDirPaths: Boolean,
+ ): DefaultListModel {
+ val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters)
+ val processedFiles = ConcurrentHashMap.newKeySet()
+ val priorityQueue = PriorityQueue(
+ fileListLimit + 1,
+ compareBy { it.getScore(prioritizeShorterDirPaths) })
+
+ val queueLock = Any()
+ var minimumScore: Int? = null
+
+ val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
+ val parallelism = (cores - 1).coerceIn(1, 8)
+
+ coroutineScope {
+ val ch = Channel(capacity = parallelism * 2)
+
+ repeat(parallelism) {
+ launch {
+ for (iterationFile in ch) {
+ val matchConfig = MatchConfig(
+ liveSettingsComponent.tolerance.getIntSpinner().value as Int,
+ liveSettingsComponent.multiMatchActive.getCheckBox().isSelected,
+ liveSettingsComponent.matchWeightSingleChar.getIntSpinner().value as Int,
+ liveSettingsComponent.matchWeightStreakModifier.getIntSpinner().value as Int,
+ liveSettingsComponent.matchWeightPartialPath.getIntSpinner().value as Int,
+ liveSettingsComponent.matchWeightFilename.getIntSpinner().value as Int
+ )
+
+ val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss, matchConfig)
+ container?.let { fuzzyMatchContainer ->
+ synchronized(queueLock) {
+ minimumScore = priorityQueue.maybeAdd(
+ minimumScore, fuzzyMatchContainer, fileListLimit, prioritizeShorterDirPaths
+ )
+ }
+ }
+ }
+ }
+ }
+
+ fileEntries.filter { processedFiles.add(it.path) }.forEach { ch.send(it) }
+ ch.close()
+ }
+
+
+ val result = DefaultListModel()
+ result.addAll(
+ priorityQueue.sortedWith(
+ compareByDescending { it.getScore(prioritizeShorterDirPaths) })
+ )
+ return result
+ }
+
+ private fun PriorityQueue.maybeAdd(
+ minimumScore: Int?,
+ fuzzyMatchContainer: FuzzyMatchContainer,
+ fileListLimit: Int,
+ prioritizeShorterDirPaths: Boolean,
+ ): Int? {
+ var ret = minimumScore
+
+ if (minimumScore == null || fuzzyMatchContainer.getScore(prioritizeShorterDirPaths) > minimumScore) {
+ this.add(fuzzyMatchContainer)
+ if (this.size > fileListLimit) {
+ this.remove()
+ ret = this.peek().getScore(prioritizeShorterDirPaths)
+ }
+ }
+
+ return ret
+ }
}
\ 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 f234e87b..fcad1371 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt
@@ -1,26 +1,26 @@
/*
-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.
-*/
+ * 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 com.intellij.util.xmlb.Converter
@@ -28,19 +28,16 @@ import com.mituuz.fuzzier.settings.FuzzierConfiguration.END_STYLE_TAG
import com.mituuz.fuzzier.settings.FuzzierConfiguration.startStyleTag
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.ObjectInputStream
-import java.io.ObjectOutputStream
-import java.io.Serializable
-import java.util.Base64
+import java.io.*
+import java.util.*
import javax.swing.DefaultListModel
class FuzzyMatchContainer(
val score: FuzzyScore,
filePath: String,
filename: String,
- moduleBasePath: String
+ moduleBasePath: String,
+ private val fileType: FileType = FileType.FILE
) : FuzzyContainer(filePath, moduleBasePath, filename) {
override fun getDisplayString(state: FuzzierGlobalSettingsService.State): String {
return when (state.filenameType) {
@@ -80,7 +77,7 @@ class FuzzyMatchContainer(
}
if (highlightIndex < source.length) {
- if (i + 1 < hlIndexes.size && hlIndexes[i + 1] == highlightIndex + 1) {
+ if (i + 1 < hlIndexes.size && hlIndexes[i + 1] == highlightIndex + 1) {
i++
continue
}
@@ -95,27 +92,15 @@ class FuzzyMatchContainer(
return stringBuilder.toString()
}
- fun highlightLegacy(source: String): String {
- val stringBuilder: StringBuilder = StringBuilder(source)
- var offset = 0
- val hlIndexes = score.highlightCharacters.sorted()
- for (i in hlIndexes) {
- if (i < source.length) {
- stringBuilder.insert(i + offset, startStyleTag)
- offset += startStyleTag.length
- stringBuilder.insert(i + offset + 1, END_STYLE_TAG)
- offset += END_STYLE_TAG.length
- }
+ fun getScore(prioritizeShorterFilePaths: Boolean): Int {
+ if (prioritizeShorterFilePaths && fileType == FileType.DIR) {
+ return getScoreWithDirLength()
}
- return stringBuilder.toString()
- }
-
- fun getScore(): Int {
return score.getTotalScore()
}
/**
- * Gets score that is prioritizing shorter dir paths
+ * Gets a score prioritizing shorter dir paths
*/
fun getScoreWithDirLength(): Int {
return score.getTotalScore() + filePath.length * -5
@@ -134,7 +119,7 @@ class FuzzyMatchContainer(
}
override fun toString(): String {
- return "FuzzyMatchContainer(basePath='$basePath', filePath='$filePath', score=${getScore()}, dirScore=${getScoreWithDirLength()})"
+ return "FuzzyMatchContainer(basePath='$basePath', filePath='$filePath', score=${getScore(false)}, dirScore=${getScoreWithDirLength()})"
}
/**
@@ -171,7 +156,7 @@ class FuzzyMatchContainer(
}
fun toFuzzyMatchContainer(): FuzzyMatchContainer {
- return FuzzyMatchContainer(score!!, filePath!!, filename!!, moduleBasePath!!)
+ return FuzzyMatchContainer(score!!, filePath!!, filename!!, moduleBasePath!!, FileType.FILE)
}
var score: FuzzyScore? = null
@@ -193,7 +178,7 @@ class FuzzyMatchContainer(
* @see FuzzierSettingsService
*/
class SerializedMatchContainerConverter : Converter>() {
- override fun fromString(value: String) : DefaultListModel {
+ override fun fromString(value: String): DefaultListModel {
// Fallback to an empty list if deserialization fails
try {
val data = Base64.getDecoder().decode(value)
@@ -206,10 +191,15 @@ class FuzzyMatchContainer(
}
}
- override fun toString(value: DefaultListModel) : String {
+ override fun toString(value: DefaultListModel): String {
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(value) }
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())
}
}
+
+ enum class FileType {
+ DIR,
+ FILE
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt
new file mode 100644
index 00000000..e5dc366b
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt
@@ -0,0 +1,32 @@
+/*
+ * 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
+
+data class IterationEntry(
+ val name: String,
+ val path: String,
+ val module: String,
+ val isDir: Boolean
+)
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
new file mode 100644
index 00000000..7699f021
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt
@@ -0,0 +1,34 @@
+/*
+ * 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
+
+data class MatchConfig(
+ val tolerance: Int = 0,
+ val multiMatch: Boolean = false,
+ val matchWeightSingleChar: Int = 1,
+ val matchWeightStreakModifier: Int = 5,
+ val matchWeightPartialPath: Int = 10,
+ val matchWeightFilename: Int = 20,
+)
\ 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 2f650080..e88d70f0 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/ScoreCalculator.kt
@@ -1,34 +1,35 @@
/*
-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.
-*/
+ * 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 com.intellij.openapi.components.service
import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FuzzyScore
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import org.apache.commons.lang3.StringUtils
-class ScoreCalculator(searchString: String) {
+class ScoreCalculator(
+ searchString: String,
+ private val config: MatchConfig
+) {
private val lowerSearchString: String = searchString.lowercase()
private val searchStringParts = lowerSearchString.split(" ")
private lateinit var fuzzyScore: FuzzyScore
@@ -39,15 +40,6 @@ class ScoreCalculator(searchString: String) {
var filePathIndex: Int = 0
private var filenameIndex: Int = 0
- // Set up match settings
- private val globalState = service().state
- private var tolerance = globalState.tolerance
- private var multiMatch = globalState.multiMatch
- private var matchWeightSingleChar = globalState.matchWeightSingleChar
- private var matchWeightStreakModifier = globalState.matchWeightStreakModifier
- private var matchWeightPartialPath = globalState.matchWeightPartialPath
- private var matchWeightFilename = globalState.matchWeightFilename
-
var currentFilePath = ""
private var longestStreak: Int = 0
private var currentStreak: Int = 0
@@ -66,7 +58,7 @@ class ScoreCalculator(searchString: String) {
toleranceCount = 0
// Check if the search string is longer than the file path, which results in no match
- if (lowerSearchString.length > (currentFilePath.length + tolerance)) {
+ if (lowerSearchString.length > (currentFilePath.length + config.tolerance)) {
return null
}
@@ -82,13 +74,13 @@ class ScoreCalculator(searchString: String) {
}
}
- if (multiMatch) {
+ if (config.multiMatch) {
calculateMultiMatchScore()
}
calculateFilenameScore()
- fuzzyScore.streakScore = (longestStreak * matchWeightStreakModifier) / 10
- fuzzyScore.filenameScore = (longestFilenameStreak * matchWeightFilename) / 10
+ fuzzyScore.streakScore = (longestStreak * config.matchWeightStreakModifier) / 10
+ fuzzyScore.filenameScore = (longestFilenameStreak * config.matchWeightFilename) / 10
return fuzzyScore
}
@@ -114,13 +106,13 @@ class ScoreCalculator(searchString: String) {
}
private fun calculateMultiMatchScore() {
- fuzzyScore.multiMatchScore += (currentFilePath.count { it in uniqueLetters } * matchWeightSingleChar) / 10
+ fuzzyScore.multiMatchScore += (currentFilePath.count { it in uniqueLetters } * config.matchWeightSingleChar) / 10
}
private fun calculatePartialPathScore(searchStringPart: String) {
StringUtils.split(currentFilePath, "/.").forEach {
if (it == searchStringPart) {
- fuzzyScore.partialPathScore += matchWeightPartialPath
+ fuzzyScore.partialPathScore += config.matchWeightPartialPath
}
}
}
@@ -134,7 +126,7 @@ class ScoreCalculator(searchString: String) {
searchStringIndex++
updateStreak(true)
} else {
- if (currentStreak > 0 && toleranceCount < tolerance) {
+ if (currentStreak > 0 && toleranceCount < config.tolerance) {
// When hitting tolerance increment the search string and filepath, but do not add streak
searchStringIndex++
toleranceCount++
@@ -196,30 +188,6 @@ class ScoreCalculator(searchString: String) {
fun canSearchStringBeContained(): Boolean {
val remainingSearchStringLength = searchStringLength - searchStringIndex
val remainingFilePathLength = currentFilePath.length - filePathIndex
- return remainingSearchStringLength <= (remainingFilePathLength + tolerance)
- }
-
- fun setMatchWeightStreakModifier(value: Int) {
- matchWeightStreakModifier = value
- }
-
- fun setMatchWeightSingleChar(value: Int) {
- matchWeightSingleChar = value
- }
-
- fun setMatchWeightPartialPath(value: Int) {
- matchWeightPartialPath = value
- }
-
- fun setFilenameMatchWeight(value: Int) {
- matchWeightFilename = value
- }
-
- fun setMultiMatch(value: Boolean) {
- multiMatch = value
- }
-
- fun setTolerance(value: Int) {
- tolerance = value
+ return remainingSearchStringLength <= (remainingFilePathLength + config.tolerance)
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
index 6962dc65..d63fc673 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt
@@ -1,151 +1,67 @@
/*
-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.
-*/
+ * 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 com.intellij.openapi.roots.ContentIterator
-import com.intellij.openapi.vcs.changes.ChangeListManager
-import com.intellij.openapi.vfs.VirtualFile
-import com.mituuz.fuzzier.util.FuzzierUtil
-import java.util.concurrent.Future
-import javax.swing.DefaultListModel
+import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FileType
/**
* Handles creating the content iterators used for string handling and excluding files
* @param exclusionList exclusion list from settings
- * @param changeListManager handles VCS check if file is being tracked. Null if VCS search should not be used
*/
class StringEvaluator(
private var exclusionList: Set,
private var modules: Map,
- private var changeListManager: ChangeListManager? = null
) {
- lateinit var scoreCalculator: ScoreCalculator
-
- fun getContentIterator(
- moduleName: String, searchString: String, listModel: DefaultListModel,
- task: Future<*>?
- ): ContentIterator {
- scoreCalculator = ScoreCalculator(searchString)
- return ContentIterator { file: VirtualFile ->
- if (task?.isCancelled == true) {
- return@ContentIterator false
- }
- if (!file.isDirectory) {
- val moduleBasePath = modules[moduleName] ?: return@ContentIterator true
-
- val filePath = file.path.removePrefix(moduleBasePath)
- if (isExcluded(file, filePath)) {
- return@ContentIterator true
- }
- if (filePath.isNotBlank()) {
- val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator)
- if (fuzzyMatchContainer != null) {
- listModel.addElement(fuzzyMatchContainer)
- }
- }
- }
- true
- }
- }
+ fun evaluateIteratorEntry(
+ iteratorEntry: IterationEntry,
+ searchString: String,
+ matchConfig: MatchConfig
+ ): FuzzyMatchContainer? {
+ val scoreCalculator = ScoreCalculator(searchString, matchConfig)
+ val moduleName = iteratorEntry.module
- fun getDirIterator(
- moduleName: String, searchString: String, listModel: DefaultListModel,
- task: Future<*>?
- ): ContentIterator {
- scoreCalculator = ScoreCalculator(searchString)
- return ContentIterator { file: VirtualFile ->
- if (task?.isCancelled == true) {
- return@ContentIterator false
- }
- if (file.isDirectory) {
- val moduleBasePath = modules[moduleName] ?: return@ContentIterator true
- val filePath = getDirPath(file, moduleBasePath, moduleName)
- if (isExcluded(file, filePath)) {
- return@ContentIterator true
- }
- if (filePath.isNotBlank()) {
- val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator)
- if (fuzzyMatchContainer != null) {
- listModel.addElement(fuzzyMatchContainer)
- }
- }
- }
- true
- }
- }
+ val moduleBasePath = modules[moduleName] ?: return null
- fun evaluateFile(
- iterationFile: FuzzierUtil.IterationFile, listModel: DefaultListModel,
- searchString: String
- ) {
- val scoreCalculator = ScoreCalculator(searchString)
- val file = iterationFile.file
- val moduleName = iterationFile.module
- if (!file.isDirectory) {
- val moduleBasePath = modules[moduleName] ?: return
+ val filePath = iteratorEntry.path.removePrefix(moduleBasePath)
+ if (isExcluded(filePath)) return null
- val filePath = file.path.removePrefix(moduleBasePath)
- if (isExcluded(file, filePath)) {
- return
- }
- if (filePath.isNotBlank()) {
- val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator)
- if (fuzzyMatchContainer != null) {
- listModel.addElement(fuzzyMatchContainer)
- }
- }
+ val fileType = if (iteratorEntry.isDir) FileType.DIR else FileType.FILE
+ if (filePath.isNotBlank()) {
+ return createFuzzyContainer(filePath, moduleBasePath, scoreCalculator, fileType)
}
- }
- private fun getDirPath(virtualFile: VirtualFile, basePath: String, module: String): String {
- var res = virtualFile.path.removePrefix(basePath)
- // Handle project root as a special case
- if (res == "") {
- res = "/"
- }
- if (res == "/$module") {
- res = "/$module/"
- }
- return res
+ return null
}
/**
* Checks if file should be excluded from the results.
*
- * If change list manager is set, only use it to decide if file should be ignored.
- *
- * Otherwise, use exclusion list from settings.
- *
- * @param file virtual file to check with change list manager
* @param filePath to check against the exclusion list
*
- * @return true if file should be excluded
+ * @return true if filePath should be excluded
*/
- private fun isExcluded(file: VirtualFile, filePath: String): Boolean {
- if (changeListManager !== null) {
- return changeListManager!!.isIgnoredFile(file)
- }
+ private fun isExcluded(filePath: String): Boolean {
return exclusionList.any { e ->
when {
e.startsWith("*") -> filePath.endsWith(e.substring(1))
@@ -161,12 +77,13 @@ class StringEvaluator(
*/
private fun createFuzzyContainer(
filePath: String, moduleBasePath: String,
- scoreCalculator: ScoreCalculator
+ scoreCalculator: ScoreCalculator,
+ fileType: FileType
): FuzzyMatchContainer? {
val filename = filePath.substring(filePath.lastIndexOf("/") + 1)
return when (val score = scoreCalculator.calculateScore(filePath)) {
null -> null
- else -> FuzzyMatchContainer(score, filePath, filename, moduleBasePath)
+ else -> FuzzyMatchContainer(score, filePath, filename, moduleBasePath, fileType)
}
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt
new file mode 100644
index 00000000..991cca57
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.intellij.iteration
+
+import com.intellij.openapi.roots.FileIndex
+import com.intellij.openapi.vfs.VirtualFile
+import com.mituuz.fuzzier.entities.IterationEntry
+
+class IntelliJIterationFileCollector : IterationFileCollector {
+ override fun collectFiles(
+ targets: List>,
+ shouldContinue: () -> Boolean,
+ fileFilter: (VirtualFile) -> Boolean
+ ): List = buildList {
+ for ((fileIndex, moduleName) in targets) {
+ fileIndex.iterateContent { vf ->
+ if (!shouldContinue()) return@iterateContent false
+
+ if (fileFilter(vf)) {
+ val iteratorEntry = IterationEntry(vf.name, vf.path, moduleName, vf.isDirectory)
+ add(iteratorEntry)
+ }
+
+ true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt
new file mode 100644
index 00000000..b142c938
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.intellij.iteration
+
+import com.intellij.openapi.roots.FileIndex
+import com.intellij.openapi.vfs.VirtualFile
+import com.mituuz.fuzzier.entities.IterationEntry
+
+interface IterationFileCollector {
+ fun collectFiles(
+ targets: List>,
+ shouldContinue: () -> Boolean,
+ fileFilter: (VirtualFile) -> Boolean,
+ ): List
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/search/ResultsProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/search/ResultsProvider.kt
new file mode 100644
index 00000000..792605a0
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/search/ResultsProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.search
+
+interface ResultsProvider {
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
index 8d11bdb3..8809cb38 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt
@@ -30,6 +30,7 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsContexts
import com.mituuz.fuzzier.components.FuzzierGlobalSettingsComponent
import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType
+import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.PopupSizing
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode
import javax.swing.JComponent
@@ -47,8 +48,11 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
component = FuzzierGlobalSettingsComponent(uiDisposable!!)
component.newTabSelect.getCheckBox().isSelected = state.newTab
component.recentFileModeSelector.getRecentFilesTypeComboBox().selectedIndex = state.recentFilesMode.ordinal
+ component.popupSizingSelector.getPopupSizingComboBox().selectedIndex = state.popupSizing.ordinal
component.defaultDimension.getIntSpinner(4).value = state.defaultPopupHeight
component.defaultDimension.getIntSpinner(1).value = state.defaultPopupWidth
+ component.autoSizePercentages.getIntSpinner(4).value = state.autoHeightPercent
+ component.autoSizePercentages.getIntSpinner(1).value = state.autoWidthPercent
component.searchPosition.getSearchPositionComboBox().selectedIndex = state.searchPosition.ordinal
component.prioritizeShortDirs.getCheckBox().isSelected = state.prioritizeShorterDirPaths
component.debounceTimerValue.getIntSpinner().value = state.debouncePeriod
@@ -65,6 +69,13 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
component.fileListSpacing.getIntSpinner().value = state.fileListSpacing
component.fuzzyGrepShowFullFile.getCheckBox().isSelected = state.fuzzyGrepShowFullFile
+ // Hide dimension settings when Auto size is selected
+ updateDimensionVisibility(state.popupSizing)
+ component.popupSizingSelector.getPopupSizingComboBox().addItemListener {
+ val selected = component.popupSizingSelector.getPopupSizingComboBox().selectedItem as PopupSizing
+ updateDimensionVisibility(selected)
+ }
+
component.tolerance.getIntSpinner().value = state.tolerance
component.multiMatchActive.getCheckBox().isSelected = state.multiMatch
component.matchWeightPartialPath.getIntSpinner().value = state.matchWeightPartialPath
@@ -75,6 +86,15 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
return component.jPanel
}
+ private fun updateDimensionVisibility(sizing: PopupSizing) {
+ val vanillaVisible = sizing != PopupSizing.AUTO_SIZE
+ val autoVisible = sizing == PopupSizing.AUTO_SIZE
+ component.defaultDimension.label.isVisible = vanillaVisible
+ component.defaultDimension.component.isVisible = vanillaVisible
+ component.autoSizePercentages.label.isVisible = autoVisible
+ component.autoSizePercentages.component.isVisible = autoVisible
+ }
+
override fun isModified(): Boolean {
val newGlobalSet = component.globalExclusionTextArea.text
.lines()
@@ -84,8 +104,11 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
return state.newTab != component.newTabSelect.getCheckBox().isSelected
|| state.recentFilesMode != component.recentFileModeSelector.getRecentFilesTypeComboBox().selectedItem
+ || state.popupSizing != component.popupSizingSelector.getPopupSizingComboBox().selectedItem
|| state.defaultPopupHeight != component.defaultDimension.getIntSpinner(4).value
|| state.defaultPopupWidth != component.defaultDimension.getIntSpinner(1).value
+ || state.autoHeightPercent != component.autoSizePercentages.getIntSpinner(4).value
+ || state.autoWidthPercent != component.autoSizePercentages.getIntSpinner(1).value
|| state.searchPosition != component.searchPosition.getSearchPositionComboBox().selectedItem
|| state.prioritizeShorterDirPaths != component.prioritizeShortDirs.getCheckBox().isSelected
|| state.debouncePeriod != component.debounceTimerValue.getIntSpinner().value
@@ -112,9 +135,13 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
state.newTab = component.newTabSelect.getCheckBox().isSelected
state.recentFilesMode =
RecentFilesMode.entries.toTypedArray()[component.recentFileModeSelector.getRecentFilesTypeComboBox().selectedIndex]
+ state.popupSizing =
+ PopupSizing.entries.toTypedArray()[component.popupSizingSelector.getPopupSizingComboBox().selectedIndex]
val newPopupHeight = component.defaultDimension.getIntSpinner(4).value as Int
val newPopupWidth = component.defaultDimension.getIntSpinner(1).value as Int
+ val newAutoHeightPercent = component.autoSizePercentages.getIntSpinner(4).value as Int
+ val newAutoWidthPercent = component.autoSizePercentages.getIntSpinner(1).value as Int
val newSearchPosition: FuzzierGlobalSettingsService.SearchPosition =
FuzzierGlobalSettingsService.SearchPosition.entries.toTypedArray()[component.searchPosition.getSearchPositionComboBox().selectedIndex]
if (state.searchPosition != newSearchPosition ||
@@ -128,6 +155,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable {
}
state.defaultPopupHeight = newPopupHeight
state.defaultPopupWidth = newPopupWidth
+ state.autoHeightPercent = newAutoHeightPercent
+ state.autoWidthPercent = newAutoWidthPercent
state.searchPosition = newSearchPosition
state.prioritizeShorterDirPaths = component.prioritizeShortDirs.getCheckBox().isSelected
state.debouncePeriod = component.debounceTimerValue.getIntSpinner().value as Int
diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
index e76e410c..88887a18 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt
@@ -45,6 +45,11 @@ class FuzzierGlobalSettingsService : PersistentStateComponent Unit,
+ actionId: String = "fuzzier.activateSelection"
+ ) {
+ bindDoubleClick(component, onActivate)
+ bindEnter(component, onActivate, actionId)
+ }
+
+ private fun bindDoubleClick(component: FuzzyComponent, onActivate: () -> Unit) {
+ component.fileList.addMouseListener(object : MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (e.clickCount == 2) onActivate()
+ }
+ })
+ }
+
+ private fun bindEnter(
+ component: FuzzyComponent,
+ onActivate: () -> Unit,
+ actionId: String,
+ ) {
+ val enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
+ val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
+ inputMap.put(enterKeyStroke, actionId)
+
+ component.searchField.actionMap.put(actionId, object : AbstractAction() {
+ override fun actionPerformed(e: ActionEvent?) {
+ onActivate()
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt
new file mode 100644
index 00000000..d9177cb1
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.popup.JBPopup
+import com.intellij.openapi.ui.popup.JBPopupFactory
+import com.intellij.openapi.wm.WindowManager
+import java.awt.Component
+import java.awt.Dimension
+import javax.swing.JComponent
+
+class AutoSizePopupProvider(
+ private val widthFactor: Double,
+ private val heightFactor: Double,
+ windowManager: WindowManager = WindowManager.getInstance(),
+ popupFactory: JBPopupFactory = JBPopupFactory.getInstance(),
+) : PopupProviderBase(windowManager, popupFactory) {
+
+ override fun show(
+ project: Project,
+ content: JComponent,
+ focus: JComponent,
+ config: PopupConfig,
+ cleanupFunction: () -> Unit,
+ ): JBPopup? {
+ val mainWindow: Component = getMainWindow(project)
+ ?: return null
+
+ val popupSize = computePopupSize(mainWindow)
+
+ val popup = baseBuilder(content, focus, config.title)
+ .createPopup()
+
+ popup.size = popupSize
+ popup.showInCenterOf(mainWindow)
+
+ popup.addListener(createCleanupListener(cleanupFunction))
+
+ return popup
+ }
+
+ internal fun computePopupSize(mainWindow: Component): Dimension {
+ val windowWidth = (mainWindow.width * widthFactor).toInt()
+ val windowHeight = (mainWindow.height * heightFactor).toInt()
+ return Dimension(windowWidth, windowHeight)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt
new file mode 100644
index 00000000..d4aa12f3
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.popup.JBPopup
+import com.intellij.openapi.ui.popup.JBPopupFactory
+import com.intellij.openapi.util.DimensionService
+import com.intellij.openapi.wm.WindowManager
+import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey
+import java.awt.Component
+import javax.swing.JComponent
+
+class DefaultPopupProvider(
+ windowManager: WindowManager = WindowManager.getInstance(),
+ popupFactory: JBPopupFactory = JBPopupFactory.getInstance(),
+ private val dimensionService: DimensionService = DimensionService.getInstance(),
+) : PopupProviderBase(windowManager, popupFactory) {
+ override fun show(
+ project: Project,
+ content: JComponent,
+ focus: JComponent,
+ config: PopupConfig,
+ cleanupFunction: () -> Unit,
+ ): JBPopup? {
+ val mainWindow: Component = getMainWindow(project)
+ ?: return null
+
+ val screenBounds = mainWindow.graphicsConfiguration.bounds
+ val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds)
+
+ if (config.resetWindow()) {
+ dimensionService.setSize(screenDimensionKey, config.preferredSizeProvider, null)
+ dimensionService.setLocation(screenDimensionKey, null, null)
+ config.clearResetWindowFlag()
+ }
+
+ val popup = baseBuilder(content, focus, config.title)
+ .setDimensionServiceKey(null, screenDimensionKey, true)
+ .createPopup()
+
+ popup.showInCenterOf(mainWindow)
+
+ popup.addListener(createCleanupListener(cleanupFunction))
+
+ return popup
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt
new file mode 100644
index 00000000..eac1c25c
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.popup.JBPopup
+import java.awt.Dimension
+import javax.swing.JComponent
+
+data class PopupConfig(
+ val title: String,
+ val dimensionKey: String,
+ val preferredSizeProvider: Dimension,
+ val resetWindow: () -> Boolean,
+ val clearResetWindowFlag: () -> Unit,
+)
+
+interface PopupProvider {
+ fun show(
+ project: Project,
+ content: JComponent,
+ focus: JComponent,
+ config: PopupConfig,
+ cleanupFunction: () -> Unit
+ ): JBPopup?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProviderBase.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProviderBase.kt
new file mode 100644
index 00000000..e8725d0f
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProviderBase.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.popup.ComponentPopupBuilder
+import com.intellij.openapi.ui.popup.JBPopupFactory
+import com.intellij.openapi.ui.popup.JBPopupListener
+import com.intellij.openapi.ui.popup.LightweightWindowEvent
+import com.intellij.openapi.wm.WindowManager
+import java.awt.Component
+import javax.swing.JComponent
+
+abstract class PopupProviderBase(
+ protected val windowManager: WindowManager = WindowManager.getInstance(),
+ protected val popupFactory: JBPopupFactory = JBPopupFactory.getInstance(),
+) : PopupProvider {
+
+ internal fun getMainWindow(project: Project): Component? = windowManager.getIdeFrame(project)?.component
+
+ internal fun baseBuilder(content: JComponent, focus: JComponent, title: String): ComponentPopupBuilder =
+ popupFactory
+ .createComponentPopupBuilder(content, focus)
+ .setFocusable(true)
+ .setRequestFocus(true)
+ .setResizable(true)
+ .setTitle(title)
+ .setMovable(true)
+ .setShowBorder(true)
+
+ internal fun createCleanupListener(cleanupFunction: () -> Unit): JBPopupListener = object : JBPopupListener {
+ override fun onClosed(event: LightweightWindowEvent) {
+ cleanupFunction()
+ }
+ }
+}
diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt
index 381a171e..1687d875 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt
@@ -1,52 +1,37 @@
/*
-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.
-*/
+ * 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.util
import com.intellij.openapi.components.service
+import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
-import com.mituuz.fuzzier.entities.FuzzyMatchContainer
import com.mituuz.fuzzier.settings.FuzzierSettingsService
-import java.util.*
-import javax.swing.DefaultListModel
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.roots.FileIndex
-import com.intellij.openapi.vfs.VirtualFile
-import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import java.awt.Rectangle
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Future
class FuzzierUtil {
- private var globalState = service().state
- private var listLimit: Int = globalState.fileListLimit
- private var prioritizeShorterDirPaths = globalState.prioritizeShorterDirPaths
-
- data class IterationFile(val file: VirtualFile, val module: String)
-
companion object {
/**
* Create a dimension key for a specific screen bounds
@@ -56,22 +41,6 @@ class FuzzierUtil {
return "${baseDimensionKey}_${screenBounds.width}_${screenBounds.height}_${screenBounds.x}_${screenBounds.y}"
}
- fun fileIndexToIterationFile(
- iterationFiles: ConcurrentHashMap.KeySetView,
- fileIndex: FileIndex, moduleName: String, task: Future<*>?,
- isDir: Boolean = false
- ) {
- fileIndex.iterateContent { file ->
- if (task?.isCancelled == true) {
- return@iterateContent false
- }
- if (file.isDirectory == isDir) {
- iterationFiles.add(IterationFile(file, moduleName))
- }
- true
- }
- }
-
fun cleanSearchString(ss: String, ignoredChars: String): String {
var ret = ss.lowercase()
for (i in ignoredChars.toSet()) {
@@ -82,62 +51,6 @@ class FuzzierUtil {
}
}
- /**
- * Process all the elements in the listModel with a priority queue to limit the size
- * and keeping the data sorted at all times
- *
- * Priority queue's size is limit + 1 to prevent any resizing
- * Only add entries to the queue if they have larger score than the minimum in the queue
- *
- * @param listModel to limit and sort
- * @param isDirSort defaults to false, enables using different sort for directories
- *
- * @return a sorted and sized list model
- */
- fun sortAndLimit(
- listModel: DefaultListModel,
- isDirSort: Boolean = false
- ): DefaultListModel {
- val useShortDirPath = isDirSort && prioritizeShorterDirPaths
- var comparator = getComparator(useShortDirPath, false)
- val priorityQueue = PriorityQueue(listLimit + 1, comparator)
-
- var minimumScore: Int? = null
- listModel.elements().toList().forEach {
- if (it is FuzzyMatchContainer) {
- if (minimumScore == null || it.getScore() > minimumScore) {
- priorityQueue.add(it)
- if (priorityQueue.size > listLimit) {
- priorityQueue.remove()
- minimumScore = priorityQueue.peek().getScore()
- }
- }
- }
- }
-
- comparator = getComparator(useShortDirPath, true)
- val result = DefaultListModel()
- result.addAll(priorityQueue.toList().sortedWith(comparator))
-
- return result
- }
-
- private fun getComparator(useShortDirPath: Boolean, isDescending: Boolean): Comparator {
- return if (isDescending) {
- compareByDescending { if (useShortDirPath) it.getScoreWithDirLength() else it.getScore() }
- } else {
- compareBy { if (useShortDirPath) it.getScoreWithDirLength() else it.getScore() }
- }
- }
-
- fun setListLimit(listLimit: Int) {
- this.listLimit = listLimit
- }
-
- fun setPrioritizeShorterDirPaths(prioritizeShortedFilePaths: Boolean) {
- this.prioritizeShorterDirPaths = prioritizeShortedFilePaths
- }
-
/**
* For each module in the project, check if the file path contains the module path.
* @return a pair of the file path (with the module path removed) and the module path
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 823d13d1..5755d0d7 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,89 +1,115 @@
+
+
- com.mituuz.fuzzier
- Fuzzier
- MituuZ
+ com.mituuz.fuzzier
+ Fuzzier
+ MituuZ
-
- A fuzzy file finder modeled after telescope for nvim. Designed to be completely usable through the keyboard (and ideavim).
+
+ A fuzzy file finder modeled after telescope for nvim. Designed to be completely usable through the
+ keyboard (and ideavim).
+
-
- com.intellij.modules.platform
+
+ com.intellij.modules.platform
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt
deleted file mode 100644
index 2bbf2242..00000000
--- a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
-
- 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
-
-import org.junit.jupiter.api.Assertions
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.condition.EnabledOnOs
-import org.junit.jupiter.api.condition.OS
-
-class ExcludeIgnoreTest {
- private var testUtil = TestUtil()
-
- @Test
- fun excludeListTest() {
- val filePaths = listOf("src/main.kt", "src/asd/main.kt", "src/asd/asd.kt", "src/not/asd.kt", "src/nope")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("asd", "nope"))
- Assertions.assertEquals(1, filePathContainer.size())
- Assertions.assertEquals("/main.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- fun excludeListTestNoMatches() {
- val filePaths = listOf("src/main.kt", "src/not.kt", "src/dsa/not.kt")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("asd"))
- Assertions.assertEquals(3, filePathContainer.size())
- Assertions.assertEquals("/main.kt", filePathContainer.get(2).filePath)
- Assertions.assertEquals("/not.kt", filePathContainer.get(1).filePath)
- Assertions.assertEquals("/dsa/not.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- fun excludeListTestEmptyList() {
- val filePaths = listOf("src/main.kt", "src/not.kt", "src/dsa/not.kt")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf())
- Assertions.assertEquals(3, filePathContainer.size())
- Assertions.assertEquals("/main.kt", filePathContainer.get(2).filePath)
- Assertions.assertEquals("/not.kt", filePathContainer.get(1).filePath)
- Assertions.assertEquals("/dsa/not.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- fun excludeListTestStartsWith() {
- val filePaths = listOf("src/main.kt", "src/asd/main.kt", "src/asd/asd.kt", "src/not/asd.kt")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("/asd*"))
- Assertions.assertEquals(2, filePathContainer.size())
- Assertions.assertEquals("/not/asd.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- fun excludeListTestEndsWith() {
- val filePaths = listOf("src/main.log", "src/asd/main.log", "src/asd/asd.kt", "src/not/asd.kt", "src/nope")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("*.log"))
- Assertions.assertEquals(3, filePathContainer.size())
- Assertions.assertEquals("/asd/asd.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- @EnabledOnOs(OS.LINUX, OS.MAC)
- // TODO: Check this on Windows
- fun testIgnoreOneFile() {
- val filePaths = listOf("src/ignore-me.kt", "src/main.kt")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf(), listOf("src/ignore-me.kt"))
- Assertions.assertEquals(1, filePathContainer.size())
- Assertions.assertEquals("/main.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- fun testIgnoreEmptyList() {
- val filePaths = listOf("src/dir/file.txt", "src/main.kt", "src/other.kt")
- val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf(), listOf())
- Assertions.assertEquals(3, filePathContainer.size())
- Assertions.assertEquals("/dir/file.txt", filePathContainer.get(0).filePath)
- Assertions.assertEquals("/main.kt", filePathContainer.get(1).filePath)
- Assertions.assertEquals("/other.kt", filePathContainer.get(2).filePath)
- }
-
- @Test
- @EnabledOnOs(OS.LINUX, OS.MAC)
- // TODO: Check this on Windows
- fun testIgnoreMultipleFiles() {
- val filePaths = listOf("src/dir/file.txt", "src/main.kt", "src/other.kt")
- val filePathContainer =
- testUtil.setUpModuleFileIndex(filePaths, setOf(), listOf("src/dir/file.txt", "src/other.kt"))
- Assertions.assertEquals(1, filePathContainer.size())
- Assertions.assertEquals("/main.kt", filePathContainer.get(0).filePath)
- }
-
- @Test
- @EnabledOnOs(OS.LINUX, OS.MAC)
- // TODO: Check this on Windows
- fun testIgnoreInCombinationWithExclusionList() {
- /* for a FuzzierVCS action only the ignore list should be applied, and the exclusions should be skipped */
- val filePaths =
- listOf("src/dir/file.txt", "src/main.kt", "src/other.kt", "src/ignore-me.kt", "src/exclude-me.kt")
- val filePathContainer =
- testUtil.setUpModuleFileIndex(filePaths, setOf("dir", "exclude-me.kt"), listOf("src/ignore-me.kt"))
- Assertions.assertEquals(4, filePathContainer.size())
- Assertions.assertEquals("/dir/file.txt", filePathContainer.get(0).filePath)
- Assertions.assertEquals("/main.kt", filePathContainer.get(1).filePath)
- Assertions.assertEquals("/other.kt", filePathContainer.get(2).filePath)
- Assertions.assertEquals("/exclude-me.kt", filePathContainer.get(3).filePath)
- }
-}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt
index cbfe5cc5..aa879c8c 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt
@@ -28,13 +28,14 @@ import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.actionSystem.EditorActionManager
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
-import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.testFramework.TestApplicationManager
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
+import com.mituuz.fuzzier.actions.FuzzyAction
import com.mituuz.fuzzier.components.SimpleFinderComponent
import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType.*
import com.mituuz.fuzzier.entities.FuzzyMatchContainer
+import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FileType.FILE
import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FuzzyScore
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
@@ -94,7 +95,7 @@ class FuzzyActionTest {
action.setFiletype(FILENAME_ONLY)
action.component = SimpleFinderComponent()
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -107,7 +108,7 @@ class FuzzyActionTest {
action.setFiletype(FILENAME_WITH_PATH)
action.component = SimpleFinderComponent()
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -121,7 +122,7 @@ class FuzzyActionTest {
action.setHighlight(false)
action.component = SimpleFinderComponent()
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -135,7 +136,7 @@ class FuzzyActionTest {
action.setHighlight(true)
action.component = SimpleFinderComponent()
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -148,7 +149,7 @@ class FuzzyActionTest {
action.setFiletype(FILE_PATH_ONLY)
action.component = SimpleFinderComponent()
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -162,7 +163,7 @@ class FuzzyActionTest {
action.component = SimpleFinderComponent()
action.component.isDirSelector = true
val renderer = action.getCellRenderer()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd", "", FILE)
val dummyList = JList()
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component)
@@ -177,7 +178,7 @@ class FuzzyActionTest {
action.component.isDirSelector = true
val renderer = action.getCellRenderer()
val dummyList = JList()
- val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd.kt", "")
+ val container = FuzzyMatchContainer(FuzzyScore(), "/src/asd", "asd.kt", "", FILE)
val expectedIcon = FileTypeManager.getInstance().getFileTypeByFileName("asd.kt").icon
val component = renderer.getListCellRendererComponent(dummyList, container, 0, false, false) as JLabel
assertNotNull(component.icon)
@@ -192,10 +193,6 @@ class FuzzyActionTest {
override fun runAction(project: Project, actionEvent: AnActionEvent) {
}
- override fun createPopup(screenDimensionKey: String): JBPopup {
- throw Exception("Not required for testing")
- }
-
override fun updateListContents(project: Project, searchString: String) {
}
}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt
index f5927a28..49cd2128 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt
@@ -1,31 +1,30 @@
/*
-
- 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.
-
+ * 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
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.ui.popup.JBPopupFactory
@@ -33,8 +32,10 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
import com.intellij.testFramework.LightVirtualFile
+import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.TestApplicationManager
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
+import com.intellij.testFramework.runInEdtAndWait
import com.mituuz.fuzzier.components.SimpleFinderComponent
import com.mituuz.fuzzier.entities.FuzzyContainer
import com.mituuz.fuzzier.entities.FuzzyMatchContainer
@@ -42,7 +43,6 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import javax.swing.DefaultListModel
import javax.swing.ListModel
-import javax.swing.PopupFactory
class FuzzyMoverTest {
@Suppress("unused")
@@ -64,17 +64,18 @@ class FuzzyMoverTest {
fuzzyMover.component.fileList.model = getListModel(virtualDir)
fuzzyMover.component.fileList.selectedIndex = 0
fuzzyMover.component.isDirSelector = true
- fuzzyMover.popup = JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
+ fuzzyMover.popup =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
ApplicationManager.getApplication().runReadAction {
fuzzyMover.movableFile = virtualFile?.let { PsiManager.getInstance(project).findFile(it) }!!
}
if (basePath != null) {
- fuzzyMover.handleInput(project).thenRun{
- var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope")
- assertNotNull(targetFile)
- targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope")
- assertNull(targetFile)
- }.join()
+ run(project)
+
+ var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope")
+ assertNotNull(targetFile)
+ targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope")
+ assertNull(targetFile)
}
myFixture.tearDown()
}
@@ -89,21 +90,21 @@ class FuzzyMoverTest {
fuzzyMover.component = SimpleFinderComponent()
val virtualFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope")
val virtualDir = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/")
- fuzzyMover.popup = JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
+ fuzzyMover.popup =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
fuzzyMover.component.fileList.model = getListModel(virtualFile)
fuzzyMover.component.fileList.selectedIndex = 0
if (basePath != null) {
- fuzzyMover.handleInput(project).join()
+ run(project)
fuzzyMover.component.fileList.model = getListModel(virtualDir)
fuzzyMover.component.fileList.selectedIndex = 0
- fuzzyMover.handleInput(project).thenRun{
- var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope")
- assertNotNull(targetFile)
- targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope")
- assertNull(targetFile)
- }.join()
+ run(project)
+ var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope")
+ assertNotNull(targetFile)
+ targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope")
+ assertNull(targetFile)
}
myFixture.tearDown()
}
@@ -123,21 +124,21 @@ class FuzzyMoverTest {
fuzzyMover.currentFile = LightVirtualFile("")
val virtualFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/main.kt")
val virtualDir = VirtualFileManager.getInstance().findFileByUrl("$basePath/test/")
- fuzzyMover.popup = JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
+ fuzzyMover.popup =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
fuzzyMover.component.fileList.model = getListModel(virtualFile)
fuzzyMover.component.fileList.selectedIndex = 0
if (basePath != null) {
- fuzzyMover.handleInput(project).join()
+ run(project)
fuzzyMover.component.fileList.model = getListModel(virtualDir)
fuzzyMover.component.fileList.selectedIndex = 0
- fuzzyMover.handleInput(project).thenRun{
- var targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/test/main.kt")
- assertNotNull(targetFile)
- targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/main.kt")
- assertNull(targetFile)
- }.join()
+ run(project)
+ var targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/test/main.kt")
+ assertNotNull(targetFile)
+ targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/main.kt")
+ assertNull(targetFile)
}
myFixture.tearDown()
}
@@ -162,25 +163,39 @@ class FuzzyMoverTest {
fuzzyMover.component.fileList.model = getListModel(virtualFile)
fuzzyMover.component.fileList.selectedIndex = 0
if (module1BasePath != null) {
- fuzzyMover.handleInput(project).join()
+ run(project)
fuzzyMover.component.fileList.model = getListModel(virtualDir)
fuzzyMover.component.fileList.selectedIndex = 0
- fuzzyMover.popup = JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
-
- fuzzyMover.handleInput(project).thenRun{
- var targetFile = VirtualFileManager.getInstance().findFileByUrl("$module2BasePath/target/MoveMe.kt")
- assertNotNull(targetFile)
- targetFile = VirtualFileManager.getInstance().findFileByUrl("$module1BasePath/MoveMe.kt")
- assertNull(targetFile)
- }.join()
+ fuzzyMover.popup =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup()
+
+ run(project)
+ var targetFile = VirtualFileManager.getInstance().findFileByUrl("$module2BasePath/target/MoveMe.kt")
+ assertNotNull(targetFile)
+ targetFile = VirtualFileManager.getInstance().findFileByUrl("$module1BasePath/MoveMe.kt")
+ assertNull(targetFile)
}
myFixture.tearDown()
}
+ private fun run(project: Project) {
+ runInEdtAndWait {
+ fuzzyMover.handleInput(project)
+ PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
+ }
+ }
+
private fun getListModel(virtualFile: VirtualFile?): ListModel {
val listModel = DefaultListModel()
if (virtualFile != null) {
- val container = FuzzyMatchContainer(FuzzyMatchContainer.FuzzyScore(), virtualFile.path, virtualFile.name, "")
+ val container =
+ FuzzyMatchContainer(
+ FuzzyMatchContainer.FuzzyScore(),
+ virtualFile.path,
+ virtualFile.name,
+ "",
+ FuzzyMatchContainer.FileType.FILE
+ )
listModel.addElement(container)
}
return listModel
diff --git a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt
new file mode 100644
index 00000000..c6786b3c
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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
+
+import com.intellij.testFramework.TestApplicationManager
+import com.mituuz.fuzzier.entities.IterationEntry
+import com.mituuz.fuzzier.entities.MatchConfig
+import com.mituuz.fuzzier.entities.StringEvaluator
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+
+class StringEvaluatorTest {
+ @Suppress("unused")
+ private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
+
+ private val moduleName = "mod1"
+ private val moduleBasePath = "/m1/src"
+
+ private fun evaluate(filePaths: List, exclusionList: Set): List {
+ val evaluator = StringEvaluator(exclusionList, mapOf(moduleName to moduleBasePath))
+
+ return filePaths.mapNotNull { fp ->
+ // Build absolute path under a fake module root so that removePrefix(moduleBasePath) works like in production
+ val relativeToSrc = fp.removePrefix("src") // e.g., "src/a/b.kt" -> "/a/b.kt"
+ val fullPath = "/m1/src$relativeToSrc"
+ val name = fullPath.substring(fullPath.lastIndexOf('/') + 1)
+ val entry = IterationEntry(name = name, path = fullPath, module = moduleName, isDir = false)
+ evaluator.evaluateIteratorEntry(
+ entry, "",
+ MatchConfig()
+ )?.filePath
+ }.sorted() // deterministic order for assertions
+ }
+
+ @Test
+ fun excludeListTest() {
+ val filePaths = listOf("src/main.kt", "src/asd/main.kt", "src/asd/asd.kt", "src/not/asd.kt", "src/nope")
+ val results = evaluate(filePaths, setOf("asd", "nope"))
+ Assertions.assertEquals(listOf("/main.kt"), results)
+ }
+
+ @Test
+ fun excludeListTestNoMatches() {
+ val filePaths = listOf("src/main.kt", "src/not.kt", "src/dsa/not.kt")
+ val results = evaluate(filePaths, setOf("asd"))
+ Assertions.assertEquals(setOf("/main.kt", "/not.kt", "/dsa/not.kt"), results.toSet())
+ }
+
+ @Test
+ fun excludeListTestEmptyList() {
+ val filePaths = listOf("src/main.kt", "src/not.kt", "src/dsa/not.kt")
+ val results = evaluate(filePaths, emptySet())
+ Assertions.assertEquals(setOf("/main.kt", "/not.kt", "/dsa/not.kt"), results.toSet())
+ }
+
+ @Test
+ fun excludeListTestStartsWith() {
+ val filePaths = listOf("src/main.kt", "src/asd/main.kt", "src/asd/asd.kt", "src/not/asd.kt")
+ val results = evaluate(filePaths, setOf("/asd*"))
+ Assertions.assertEquals(setOf("/main.kt", "/not/asd.kt"), results.toSet())
+ }
+
+ @Test
+ fun excludeListTestEndsWith() {
+ val filePaths = listOf("src/main.log", "src/asd/main.log", "src/asd/asd.kt", "src/not/asd.kt", "src/nope")
+ val results = evaluate(filePaths, setOf("*.log"))
+ Assertions.assertEquals(setOf("/asd/asd.kt", "/not/asd.kt", "/nope"), results.toSet())
+ }
+
+ @Test
+ fun testIgnoreEmptyList() {
+ val filePaths = listOf("src/dir/file.txt", "src/main.kt", "src/other.kt")
+ val results = evaluate(filePaths, emptySet())
+ Assertions.assertEquals(setOf("/dir/file.txt", "/main.kt", "/other.kt"), results.toSet())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt
index 1f06516e..6c8722ca 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt
@@ -29,8 +29,6 @@ import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.modules
import com.intellij.openapi.project.rootManager
-import com.intellij.openapi.vcs.changes.ChangeListManager
-import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
@@ -38,9 +36,6 @@ import com.intellij.testFramework.fixtures.IdeaProjectTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.runInEdtAndWait
import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.entities.StringEvaluator
-import io.mockk.every
-import io.mockk.mockk
import javax.swing.DefaultListModel
class TestUtil {
@@ -69,14 +64,12 @@ class TestUtil {
fun setUpModuleFileIndex(
filesToAdd: List,
exclusionList: Set,
- ignoredFiles: List? = null
): DefaultListModel {
val filePathContainer = DefaultListModel()
val factory = IdeaTestFixtureFactory.getFixtureFactory()
val fixtureBuilder = factory.createLightFixtureBuilder(null, "Test")
val fixture = fixtureBuilder.fixture
val myFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture)
- val stringEvaluator: StringEvaluator
myFixture.setUp()
addFilesToProject(filesToAdd, myFixture, fixture)
@@ -86,24 +79,6 @@ class TestUtil {
val module = myFixture.project.modules[0]
map[module.name] = module.rootManager.contentRoots[1].path
- if (ignoredFiles !== null) {
- val changeListManager = mockk()
- every { changeListManager.isIgnoredFile(any()) } answers {
- val file = firstArg()
- val tempDirPath = myFixture.tempDirPath
- ignoredFiles.any { ("$tempDirPath/$it") == file.path }
- }
- stringEvaluator = StringEvaluator(exclusionList, map, changeListManager)
- } else {
- stringEvaluator = StringEvaluator(exclusionList, map)
- }
-
- val contentIterator = stringEvaluator.getContentIterator(myFixture.module.name, "", filePathContainer, null)
- val index = myFixture.module.rootManager.fileIndex
- runInEdtAndWait {
- index.iterateContent(contentIterator)
- }
- // Handle clearing ProjectFileIndex between tests
myFixture.tearDown()
return filePathContainer
diff --git a/src/test/kotlin/com/mituuz/fuzzier/actions/FuzzyActionProviderSelectionTest.kt b/src/test/kotlin/com/mituuz/fuzzier/actions/FuzzyActionProviderSelectionTest.kt
new file mode 100644
index 00000000..32f1ed8b
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/actions/FuzzyActionProviderSelectionTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.actions
+
+import com.intellij.openapi.components.service
+import com.intellij.testFramework.TestApplicationManager
+import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
+import com.mituuz.fuzzier.ui.popup.AutoSizePopupProvider
+import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+private class TestFuzzyAction : FuzzyAction() {
+ override fun runAction(
+ project: com.intellij.openapi.project.Project,
+ actionEvent: com.intellij.openapi.actionSystem.AnActionEvent
+ ) { /* no-op */
+ }
+
+ override fun updateListContents(project: com.intellij.openapi.project.Project, searchString: String) { /* no-op */
+ }
+
+ fun peekProvider() = getPopupProvider()
+}
+
+class FuzzyActionProviderSelectionTest {
+ @Suppress("unused")
+ private val app = TestApplicationManager.getInstance()
+ private val state = service().state
+
+ private lateinit var action: TestFuzzyAction
+
+ @BeforeEach
+ fun setUp() {
+ action = TestFuzzyAction()
+ }
+
+ @Test
+ fun returnsAutoSizeProvider_whenAutoSizeSelected() {
+ state.popupSizing = FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE
+ val provider = action.peekProvider()
+ assertTrue(provider is AutoSizePopupProvider)
+ }
+
+ @Test
+ fun returnsDefaultProvider_whenVanillaSelected() {
+ state.popupSizing = FuzzierGlobalSettingsService.PopupSizing.VANILLA
+ val provider = action.peekProvider()
+ assertTrue(provider is DefaultPopupProvider)
+ }
+}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
index c963a945..ba089fb0 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt
@@ -1,33 +1,34 @@
/*
-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.
-*/
+ * 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 com.intellij.testFramework.TestApplicationManager
+import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FileType.FILE
import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FuzzyScore
import com.mituuz.fuzzier.settings.FuzzierConfiguration.END_STYLE_TAG
import com.mituuz.fuzzier.settings.FuzzierConfiguration.startStyleTag
-import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -44,7 +45,7 @@ class FuzzyMatchContainerTest {
val score = FuzzyScore()
score.highlightCharacters.add(0)
score.highlightCharacters.add(4)
- val container = FuzzyMatchContainer(score, "", "Hello", "")
+ val container = FuzzyMatchContainer(score, "", "Hello", "", FILE)
val res = container.highlight(container.filename)
assertEquals("${startStyleTag}H${END_STYLE_TAG}ell${startStyleTag}o$END_STYLE_TAG", res)
}
@@ -54,7 +55,7 @@ class FuzzyMatchContainerTest {
val score = FuzzyScore()
score.highlightCharacters.add(0)
score.highlightCharacters.add(1)
- val container = FuzzyMatchContainer(score, "", "Hello", "")
+ val container = FuzzyMatchContainer(score, "", "Hello", "", FILE)
val res = container.highlight(container.filename)
assertEquals("${startStyleTag}He${END_STYLE_TAG}llo", res)
}
@@ -70,7 +71,7 @@ class FuzzyMatchContainerTest {
score.highlightCharacters.add(17) // e
score.highlightCharacters.add(18) // r
- val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt", "")
+ val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt", "", FILE)
val res = container.highlight(container.filename)
val sb = StringBuilder()
@@ -90,13 +91,17 @@ class FuzzyMatchContainerTest {
@Test
fun `Test serialization`() {
val score = FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt", "")
+ val container = FuzzyMatchContainer(
+ score, "", "FuzzyMatchContainerTest.kt", "",
+ FILE
+ )
val serializableContainer = FuzzyMatchContainer.SerializedMatchContainer.fromFuzzyMatchContainer(container)
val byteArrayOutputStream = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(serializableContainer) }
val byteArrayInputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray())
- val deserialized = ObjectInputStream(byteArrayInputStream).use { it.readObject() as FuzzyMatchContainer.SerializedMatchContainer }
+ val deserialized =
+ ObjectInputStream(byteArrayInputStream).use { it.readObject() as FuzzyMatchContainer.SerializedMatchContainer }
val fmc = deserialized.toFuzzyMatchContainer()
assertEquals("", fmc.filePath)
assertEquals("FuzzyMatchContainerTest.kt", fmc.filename)
@@ -106,13 +111,17 @@ class FuzzyMatchContainerTest {
fun `Test default list serialization`() {
val list = DefaultListModel()
val score = FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt", "")
+ val container = FuzzyMatchContainer(
+ score, "", "FuzzyMatchContainerTest.kt", "",
+ FILE
+ )
list.addElement(container)
val converter = FuzzyMatchContainer.SerializedMatchContainerConverter()
val stringRep = converter.toString(FuzzyMatchContainer.SerializedMatchContainer.fromListModel(list))
- val deserialized: DefaultListModel = converter.fromString(stringRep)
+ val deserialized: DefaultListModel =
+ converter.fromString(stringRep)
assertEquals(1, deserialized.size)
assertEquals("", deserialized.get(0).filePath)
assertEquals("FuzzyMatchContainerTest.kt", deserialized.get(0).filename)
@@ -121,7 +130,8 @@ class FuzzyMatchContainerTest {
@Test
fun `Deserialization fails`() {
val converter = FuzzyMatchContainer.SerializedMatchContainerConverter()
- val deserialized: DefaultListModel = converter.fromString("This should not work")
+ val deserialized: DefaultListModel =
+ converter.fromString("This should not work")
assertEquals(0, deserialized.size)
}
}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
index 04868240..687a0432 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/entities/ScoreCalculatorTest.kt
@@ -1,39 +1,35 @@
/*
-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.
-*/
+ * 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 com.intellij.testFramework.TestApplicationManager
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class ScoreCalculatorTest {
- @Suppress("unused") // Required for the tests
- private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
-
@Test
fun `Search string contained same index`() {
- val sc = ScoreCalculator("test")
+ val sc = ScoreCalculator("test", MatchConfig())
sc.searchStringIndex = 0
sc.searchStringLength = 4
@@ -45,7 +41,7 @@ class ScoreCalculatorTest {
@Test
fun `Search string contained different index`() {
- val sc = ScoreCalculator("test")
+ val sc = ScoreCalculator("test", MatchConfig())
sc.searchStringIndex = 0
sc.searchStringLength = 4
@@ -57,82 +53,91 @@ class ScoreCalculatorTest {
@Test
fun `Basic streak happy case`() {
- val sc = ScoreCalculator("test")
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10
+ )
+ val sc = ScoreCalculator("test", matchConfig)
- sc.setMatchWeightStreakModifier(10)
val fScore = sc.calculateScore("/test")
assertEquals(4, fScore!!.streakScore)
}
@Test
fun `Basic streak longer path`() {
- val sc = ScoreCalculator("test")
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10
+ )
+ val sc = ScoreCalculator("test", matchConfig)
- sc.setMatchWeightStreakModifier(10)
val fScore = sc.calculateScore("/te/st")
assertEquals(2, fScore!!.streakScore)
}
@Test
fun `Basic streak no possible match`() {
- val sc = ScoreCalculator("test")
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10
+ )
+ val sc = ScoreCalculator("test", matchConfig)
- sc.setMatchWeightStreakModifier(10)
val fScore = sc.calculateScore("/te")
assertNull(fScore)
}
@Test
fun `Multi match basic test`() {
- val sc = ScoreCalculator("test")
+ val matchConfig = MatchConfig(
+ matchWeightSingleChar = 10, multiMatch = true
+ )
+ val sc = ScoreCalculator("test", matchConfig)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
val fScore = sc.calculateScore("/test")
assertEquals(4, fScore!!.multiMatchScore)
}
@Test
fun `Multi match basic test multiples`() {
- val sc = ScoreCalculator("test")
-
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
+ val matchConfig = MatchConfig(
+ matchWeightSingleChar = 10, multiMatch = true
+ )
+ val sc = ScoreCalculator("test", matchConfig)
val fScore = sc.calculateScore("/testtest")
assertEquals(8, fScore!!.multiMatchScore)
}
@Test
fun `Multi match basic test multiples multiples`() {
- val sc = ScoreCalculator("test test")
-
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
+ val matchConfig = MatchConfig(
+ matchWeightSingleChar = 10, multiMatch = true
+ )
+ val sc = ScoreCalculator("test test", matchConfig)
val fScore = sc.calculateScore("/testtest")
assertEquals(8, fScore!!.multiMatchScore)
}
@Test
fun `Partial path score basic test`() {
- val sc = ScoreCalculator("test")
-
- sc.setMatchWeightPartialPath(1)
+ val matchConfig = MatchConfig(
+ matchWeightPartialPath = 1
+ )
+ val sc = ScoreCalculator("test", matchConfig)
val fScore = sc.calculateScore("/test.kt")
assertEquals(1, fScore!!.partialPathScore)
}
@Test
fun `Filename score basic test`() {
- val sc = ScoreCalculator("test")
-
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("test", matchConfig)
val fScore = sc.calculateScore("/test.kt")
assertEquals(4, fScore!!.filenameScore)
}
@Test
fun `Empty ss and fp`() {
- val sc = ScoreCalculator("")
+ val sc = ScoreCalculator("", MatchConfig())
val fScore = sc.calculateScore("")
assertEquals(0, fScore!!.getTotalScore())
@@ -141,12 +146,10 @@ class ScoreCalculatorTest {
// Legacy tests
@Test
fun `Non-consecutive streak`() {
- val sc = ScoreCalculator("kif")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kif", matchConfig)
val fScore = sc.calculateScore("/KotlinIsFun")
@@ -158,12 +161,10 @@ class ScoreCalculatorTest {
@Test
fun `Consecutive streak`() {
- val sc = ScoreCalculator("kot")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kot", matchConfig)
val fScore = sc.calculateScore("/KotlinIsFun")
@@ -175,21 +176,21 @@ class ScoreCalculatorTest {
@Test
fun `Too long ss`() {
- val sc = ScoreCalculator("TooLongSearchString")
+ val sc = ScoreCalculator("TooLongSearchString", MatchConfig())
val fScore = sc.calculateScore("/KIF")
assertNull(fScore)
}
@Test
fun `No possible match`() {
- val sc = ScoreCalculator("A")
+ val sc = ScoreCalculator("A", MatchConfig())
val fScore = sc.calculateScore("/KIF")
assertNull(fScore)
}
@Test
fun `Empty ss`() {
- val sc = ScoreCalculator("")
+ val sc = ScoreCalculator("", MatchConfig())
val fScore = sc.calculateScore("/KIF")
assertEquals(0, fScore!!.getTotalScore())
@@ -197,26 +198,24 @@ class ScoreCalculatorTest {
@Test
fun `No possible match split`() {
- val sc = ScoreCalculator("A A B")
+ val sc = ScoreCalculator("A A B", MatchConfig())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
assertNull(fScore)
}
@Test
fun `Partial match split`() {
- val sc = ScoreCalculator("A A K")
+ val sc = ScoreCalculator("A A K", MatchConfig())
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
assertNull(fScore)
}
@Test
fun `Split match for space`() {
- val sc = ScoreCalculator("fun kotlin")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("fun kotlin", matchConfig)
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -228,12 +227,10 @@ class ScoreCalculatorTest {
@Test
fun `Legacy test 1`() {
- val sc = ScoreCalculator("kif")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kif", matchConfig)
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -245,12 +242,10 @@ class ScoreCalculatorTest {
@Test
fun `Legacy test 2`() {
- val sc = ScoreCalculator("kif")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kif", matchConfig)
val fScore = sc.calculateScore("/Kiffer/Is/Fun/kiffer.kt")
@@ -262,12 +257,10 @@ class ScoreCalculatorTest {
@Test
fun `Multiple partial file path matches`() {
- val sc = ScoreCalculator("kif")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kif", matchConfig)
val fScore = sc.calculateScore("/Kif/Is/Fun/kif.kt")
@@ -279,12 +272,10 @@ class ScoreCalculatorTest {
@Test
fun `Legacy test 3`() {
- val sc = ScoreCalculator("kif fun kotlin")
-
- sc.setMatchWeightStreakModifier(10)
- sc.setMultiMatch(true)
- sc.setMatchWeightSingleChar(10)
- sc.setFilenameMatchWeight(10)
+ val matchConfig = MatchConfig(
+ matchWeightStreakModifier = 10, multiMatch = true, matchWeightSingleChar = 10, matchWeightFilename = 10
+ )
+ val sc = ScoreCalculator("kif fun kotlin", matchConfig)
val fScore = sc.calculateScore("/Kotlin/Is/Fun/kif.kt")
@@ -296,64 +287,80 @@ class ScoreCalculatorTest {
@Test
fun `5 tolerance matching normal match`() {
- val sc = ScoreCalculator("kotlin")
- sc.setTolerance(5)
+ val matchConfig = MatchConfig(
+ tolerance = 5,
+ )
+ val sc = ScoreCalculator("kotlin", matchConfig)
assertNotNull(sc.calculateScore("/Kotlin"))
}
@Test
fun `5 tolerance matching 1 letter difference`() {
- val sc = ScoreCalculator("korlin")
- sc.setTolerance(5)
+ val matchConfig = MatchConfig(
+ tolerance = 5,
+ )
+ val sc = ScoreCalculator("korlin", matchConfig)
assertNotNull(sc.calculateScore("/Kotlin"))
}
@Test
fun `1 tolerance matching 1 letter difference`() {
- val sc = ScoreCalculator("korlin")
- sc.setTolerance(1)
+ val matchConfig = MatchConfig(
+ tolerance = 1,
+ )
+ val sc = ScoreCalculator("korlin", matchConfig)
assertNotNull(sc.calculateScore("/Kotlin"))
}
@Test
fun `1 tolerance matching 2 letter difference`() {
- val sc = ScoreCalculator("korlnn")
- sc.setTolerance(1)
+ val matchConfig = MatchConfig(
+ tolerance = 1,
+ )
+ val sc = ScoreCalculator("korlnn", matchConfig)
assertNull(sc.calculateScore("/Kotlin"))
}
@Test
fun `1 tolerance matching 1 letter difference with split path`() {
- val sc = ScoreCalculator("korlin")
- sc.setTolerance(1)
+ val matchConfig = MatchConfig(
+ tolerance = 1,
+ )
+ val sc = ScoreCalculator("korlin", matchConfig)
assertNotNull(sc.calculateScore("/Kot/lin"))
}
@Test
fun `1 tolerance matching 2 letter difference with split path`() {
- val sc = ScoreCalculator("korlin")
- sc.setTolerance(1)
+ val matchConfig = MatchConfig(
+ tolerance = 1,
+ )
+ val sc = ScoreCalculator("korlin", matchConfig)
assertNull(sc.calculateScore("/Kot/sin"))
}
@Test
fun `2 tolerance matching 2 letter difference with split path`() {
- val sc = ScoreCalculator("korlin")
- sc.setTolerance(2)
+ val matchConfig = MatchConfig(
+ tolerance = 2,
+ )
+ val sc = ScoreCalculator("korlin", matchConfig)
assertNotNull(sc.calculateScore("/Kot/sin"))
}
@Test
fun `Don't match longer strings even if there is tolerance left`() {
- val sc = ScoreCalculator("kotlin12345")
- sc.setTolerance(5)
+ val matchConfig = MatchConfig(
+ tolerance = 5,
+ )
+ val sc = ScoreCalculator("kotlin12345", matchConfig)
assertNull(sc.calculateScore("/Kotlin"))
}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt
new file mode 100644
index 00000000..de7eca70
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.intellij.iteration
+
+import com.intellij.openapi.roots.ContentIterator
+import com.intellij.openapi.roots.FileIndex
+import com.intellij.testFramework.LightVirtualFile
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.util.concurrent.atomic.AtomicInteger
+
+class IntelliJIteratorEntryCollectorTest {
+ private lateinit var collector: IntelliJIterationFileCollector
+
+ @BeforeEach
+ fun setUp() {
+ collector = IntelliJIterationFileCollector()
+ }
+
+ @Test
+ fun `collectFiles returns empty list when targets are empty`() {
+ val result = collector.collectFiles(
+ targets = emptyList(),
+ shouldContinue = { true },
+ fileFilter = { true },
+ )
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `stops iterating when shouldContinue becomes false`() {
+ val file1 = LightVirtualFile("a.txt")
+ val file2 = LightVirtualFile("b.txt")
+ val index = mockk()
+ val iteratorSlot = slot()
+
+ every { index.iterateContent(capture(iteratorSlot)) } answers {
+ iteratorSlot.captured.processFile(file1)
+ iteratorSlot.captured.processFile(file2)
+ true
+ }
+
+ val calls = AtomicInteger(0)
+ val res = collector.collectFiles(
+ targets = listOf(index to "mod"),
+ shouldContinue = { calls.incrementAndGet() == 1 },
+ fileFilter = { true }
+ )
+
+ assertEquals(1, res.size)
+ assertEquals("a.txt", res[0].name)
+ }
+
+ @Test
+ fun `skips files that match filter`() {
+ val file1 = LightVirtualFile("a.txt")
+ val dir = mockk()
+ val file2 = LightVirtualFile("b.txt")
+ val index = mockk()
+ val iteratorSlot = slot()
+
+ every { dir.isDirectory } returns true
+ every { index.iterateContent(capture(iteratorSlot)) } answers {
+ iteratorSlot.captured.processFile(file1)
+ iteratorSlot.captured.processFile(dir)
+ iteratorSlot.captured.processFile(file2)
+ true
+ }
+
+ val res = collector.collectFiles(
+ targets = listOf(index to "mod"),
+ shouldContinue = { true },
+ fileFilter = { vf -> !vf.isDirectory }
+ )
+
+ assertEquals(2, res.size)
+ assertEquals("a.txt", res[0].name)
+ assertEquals("b.txt", res[1].name)
+ }
+
+ @Test
+ fun `collects files from multiple file indexes`() {
+ val file1 = LightVirtualFile("a.txt")
+ val file2 = LightVirtualFile("b.txt")
+ val file3 = LightVirtualFile("c.txt")
+ val file4 = LightVirtualFile("d.txt")
+
+ val index1 = mockk()
+ val index2 = mockk()
+ val iteratorSlot1 = slot()
+ val iteratorSlot2 = slot()
+
+ every { index1.iterateContent(capture(iteratorSlot1)) } answers {
+ iteratorSlot1.captured.processFile(file1)
+ iteratorSlot1.captured.processFile(file2)
+ true
+ }
+
+ every { index2.iterateContent(capture(iteratorSlot2)) } answers {
+ iteratorSlot2.captured.processFile(file3)
+ iteratorSlot2.captured.processFile(file4)
+ true
+ }
+
+ val res = collector.collectFiles(
+ targets = listOf(index1 to "mod1", index2 to "mod2"),
+ shouldContinue = { true },
+ fileFilter = { true }
+ )
+
+ assertEquals(4, res.size)
+ assertEquals("a.txt", res[0].name)
+ assertEquals("mod1", res[0].module)
+ assertEquals("b.txt", res[1].name)
+ assertEquals("mod1", res[1].module)
+ assertEquals("c.txt", res[2].name)
+ assertEquals("mod2", res[2].module)
+ assertEquals("d.txt", res[3].name)
+ assertEquals("mod2", res[3].module)
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
index f87c8505..97d2e29c 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurableTest.kt
@@ -64,6 +64,71 @@ class FuzzierGlobalSettingsConfigurableTest {
state.matchWeightFilename = 15
}
+ @Test
+ fun popupSizing_isPopulatedFromState() {
+ state.popupSizing = FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE
+ settingsConfigurable.createComponent()
+ val selected = settingsConfigurable.component.popupSizingSelector
+ .getPopupSizingComboBox().selectedItem as FuzzierGlobalSettingsService.PopupSizing
+ assertEquals(FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE, selected)
+ }
+
+ @Test
+ fun popupSizing_isModifiedAndApply() {
+ settingsConfigurable.createComponent()
+ assertFalse(settingsConfigurable.isModified)
+
+ val newValue = if (state.popupSizing == FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE)
+ FuzzierGlobalSettingsService.PopupSizing.VANILLA else FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE
+ state.popupSizing = newValue
+ assertTrue(settingsConfigurable.isModified)
+
+ val uiSelected = settingsConfigurable.component.popupSizingSelector
+ .getPopupSizingComboBox().selectedItem as FuzzierGlobalSettingsService.PopupSizing
+ settingsConfigurable.apply()
+ assertEquals(uiSelected, state.popupSizing)
+ assertFalse(settingsConfigurable.isModified)
+ }
+
+ @Test
+ fun popupSizing_dimensionVisibilityToggles() {
+ state.popupSizing = FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE
+ settingsConfigurable.createComponent()
+ val defaultDim = settingsConfigurable.component.defaultDimension
+ assertFalse(defaultDim.label.isVisible)
+ assertFalse(defaultDim.component.isVisible)
+
+ val combo = settingsConfigurable.component.popupSizingSelector.getPopupSizingComboBox()
+ combo.selectedItem = FuzzierGlobalSettingsService.PopupSizing.VANILLA
+ settingsConfigurable.apply()
+ assertTrue(defaultDim.label.isVisible)
+ assertTrue(defaultDim.component.isVisible)
+ }
+
+ @Test
+ fun popupSizing_persistsAcrossRecreate() {
+ settingsConfigurable.createComponent()
+ val combo = settingsConfigurable.component.popupSizingSelector.getPopupSizingComboBox()
+ combo.selectedItem = FuzzierGlobalSettingsService.PopupSizing.VANILLA
+ settingsConfigurable.apply()
+
+ val recreated = FuzzierGlobalSettingsConfigurable()
+ recreated.createComponent()
+ val selected = recreated.component.popupSizingSelector.getPopupSizingComboBox().selectedItem
+ as FuzzierGlobalSettingsService.PopupSizing
+ assertEquals(FuzzierGlobalSettingsService.PopupSizing.VANILLA, selected)
+ }
+
+ @Test
+ fun settingsComponent_getPopupSizingComboBox_smoke() {
+ val configurable = FuzzierGlobalSettingsConfigurable()
+ configurable.createComponent()
+ val combo = configurable.component.popupSizingSelector.getPopupSizingComboBox()
+ assertNotNull(combo)
+ combo.selectedItem = FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE
+ assertEquals(FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE, combo.selectedItem)
+ }
+
@Test
fun newTab() {
pre()
diff --git a/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt
new file mode 100644
index 00000000..686c984d
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.ui.bindings
+
+import com.intellij.testFramework.TestApplicationManager
+import com.mituuz.fuzzier.components.FuzzyComponent
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.awt.event.MouseEvent
+import javax.swing.JComponent
+import javax.swing.KeyStroke
+
+class ActivationBindingsTest {
+ @Suppress("unused")
+ private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
+
+ private lateinit var component: FuzzyComponent
+
+ @BeforeEach
+ fun setUp() {
+ component = FuzzyComponent()
+ }
+
+ @Test
+ fun `Single-click on list doesn't trigger activation`() {
+ var activations = 0
+ ActivationBindings.install(component, onActivate = { activations++ })
+
+ // Simulate double-click on the file list
+ val evt = MouseEvent(
+ component.fileList,
+ MouseEvent.MOUSE_CLICKED,
+ System.currentTimeMillis(),
+ 0,
+ 1,
+ 1,
+ 1,
+ false
+ )
+ // Dispatch to all listeners
+ for (listener in component.fileList.mouseListeners) {
+ listener.mouseClicked(evt)
+ }
+
+ assertEquals(0, activations)
+ }
+
+ @Test
+ fun `Double-click on list triggers activation`() {
+ var activations = 0
+ ActivationBindings.install(component, onActivate = { activations++ })
+
+ // Simulate double-click on the file list
+ val evt = MouseEvent(
+ component.fileList,
+ MouseEvent.MOUSE_CLICKED,
+ System.currentTimeMillis(),
+ 0,
+ 1,
+ 1,
+ 2,
+ false
+ )
+ // Dispatch to all listeners
+ for (listener in component.fileList.mouseListeners) {
+ listener.mouseClicked(evt)
+ }
+
+ assertEquals(1, activations)
+ }
+
+ @Test
+ fun `Enter key binding triggers activation`() {
+ var activations = 0
+ val actionId = "test.activateSelection"
+ ActivationBindings.install(component, onActivate = { activations++ }, actionId = actionId)
+
+ // Verify InputMap contains the mapping for ENTER to our actionId
+ val enter = KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ENTER, 0)
+ val inputMap = component.searchField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
+ val mapped = inputMap.get(enter)
+ assertEquals(actionId, mapped)
+
+ // Retrieve and invoke the action directly from the ActionMap
+ val action = component.searchField.actionMap.get(actionId)
+ action.actionPerformed(null)
+
+ assertEquals(1, activations)
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProviderTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProviderTest.kt
new file mode 100644
index 00000000..38faa2bb
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProviderTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.ui.popup.LightweightWindow
+import com.intellij.openapi.ui.popup.LightweightWindowEvent
+import com.intellij.testFramework.TestApplicationManager
+import com.intellij.testFramework.fixtures.CodeInsightTestFixture
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.awt.Dimension
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+class AutoSizePopupProviderTest {
+ @Suppress("unused")
+ private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
+
+ private lateinit var fixture: IdeaProjectTestFixture
+ private lateinit var codeFixture: CodeInsightTestFixture
+
+ private lateinit var provider: AutoSizePopupProvider
+
+ @BeforeEach
+ fun setUp() {
+ // 50% width, 25% height
+ provider = AutoSizePopupProvider(0.5, 0.25)
+ }
+
+ @Test
+ fun `show returns null and exits when no IDE frame is available`() {
+ val factory = IdeaTestFixtureFactory.getFixtureFactory()
+ fixture = factory.createLightFixtureBuilder(null, "Test").fixture
+ codeFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture)
+ codeFixture.setUp()
+ val project = fixture.project
+ val content = JPanel()
+ val focus = JLabel("focus")
+ var cleared = false
+ val cfg = PopupConfig(
+ title = "Test",
+ dimensionKey = "fuzzier.test",
+ preferredSizeProvider = Dimension(800, 600),
+ resetWindow = { true },
+ clearResetWindowFlag = { cleared = true }
+ )
+
+ try {
+ val popup = provider.show(project, content, focus, cfg) { /* cleanup */ }
+ assertNull(popup)
+ // No side-effects should occur since we exit early before using DimensionService
+ assertEquals(false, cleared)
+ } finally {
+ codeFixture.tearDown()
+ }
+ }
+
+ @Test
+ fun `cleanup function is called when popup is closed`() {
+ var cleanupCalled = false
+ val listener = provider.createCleanupListener { cleanupCalled = true }
+
+ val lw = object : LightweightWindow {}
+
+ val evt = LightweightWindowEvent(lw, true)
+ listener.onClosed(evt)
+
+ assertEquals(true, cleanupCalled)
+ }
+
+ @Test
+ fun `computePopupSize uses factors to scale main window size`() {
+ val fakeWindow = JPanel()
+ fakeWindow.size = Dimension(1000, 800)
+
+ val size = provider.computePopupSize(fakeWindow)
+ assertEquals(500, size.width)
+ assertEquals(200, size.height)
+ }
+}
diff --git a/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt
new file mode 100644
index 00000000..e3b042f4
--- /dev/null
+++ b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.ui.popup
+
+import com.intellij.openapi.ui.popup.LightweightWindow
+import com.intellij.openapi.ui.popup.LightweightWindowEvent
+import com.intellij.testFramework.TestApplicationManager
+import com.intellij.testFramework.fixtures.CodeInsightTestFixture
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.awt.Dimension
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+class DefaultPopupProviderTest {
+ @Suppress("unused")
+ private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance()
+
+ private lateinit var provider: DefaultPopupProvider
+ private lateinit var fixture: IdeaProjectTestFixture
+ private lateinit var codeFixture: CodeInsightTestFixture
+
+ @BeforeEach
+ fun setUp() {
+ provider = DefaultPopupProvider()
+ }
+
+ @Test
+ fun `show returns null and exits when no IDE frame is available`() {
+ // Set up a lightweight project fixture to get a Project instance
+ val factory = IdeaTestFixtureFactory.getFixtureFactory()
+ fixture = factory.createLightFixtureBuilder(null, "Test").fixture
+ codeFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture)
+ codeFixture.setUp()
+ val project = fixture.project
+ val content = JPanel()
+ val focus = JLabel("focus")
+ var cleared = false
+ val cfg = PopupConfig(
+ title = "Test",
+ dimensionKey = "fuzzier.test",
+ preferredSizeProvider = Dimension(800, 600),
+ resetWindow = { true },
+ clearResetWindowFlag = { cleared = true }
+ )
+
+ try {
+ val popup = provider.show(project, content, focus, cfg) { /* cleanup */ }
+ // Should return null and not throw
+ assertNull(popup)
+ // Since we exit early due to missing IDE frame, ensure no side-effects were triggered
+ assertEquals(false, cleared)
+ } finally {
+ codeFixture.tearDown()
+ }
+ }
+
+ @Test
+ fun `cleanup function is called when popup is closed`() {
+ var cleanupCalled = false
+ val listener = provider.createCleanupListener { cleanupCalled = true }
+
+ val lw = object : LightweightWindow {}
+
+ val evt = LightweightWindowEvent(lw, true)
+ listener.onClosed(evt)
+
+ assertEquals(true, cleanupCalled)
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt b/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt
index d0b8a6c0..ae48d384 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt
@@ -1,36 +1,33 @@
/*
-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.
-*/
+ * 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.util
import com.intellij.openapi.components.service
import com.intellij.testFramework.TestApplicationManager
import com.mituuz.fuzzier.TestUtil
import com.mituuz.fuzzier.entities.FuzzyContainer
-import com.mituuz.fuzzier.entities.FuzzyMatchContainer
-import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FuzzyScore
import com.mituuz.fuzzier.settings.FuzzierSettingsService
-import kotlinx.collections.immutable.persistentMapOf
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -41,7 +38,6 @@ class FuzzierUtilTest {
private val testApplicationManager = TestApplicationManager.getInstance()
private val fuzzierUtil = FuzzierUtil()
private val listModel = DefaultListModel()
- private lateinit var result: DefaultListModel
private val testUtil = TestUtil()
@BeforeEach
@@ -51,7 +47,11 @@ class FuzzierUtilTest {
@Test
fun `Parse modules multiple modules and roots`() {
- val myFixture = testUtil.setUpMultiModuleProject(listOf("src1", "/src1/file1"), listOf("src2", "/src2/file2"), listOf("src3", "/src3/file3"))
+ val myFixture = testUtil.setUpMultiModuleProject(
+ listOf("src1", "/src1/file1"),
+ listOf("src2", "/src2/file2"),
+ listOf("src3", "/src3/file3")
+ )
fuzzierUtil.parseModules(myFixture.project)
val modules = myFixture.project.service().state.modules
@@ -64,7 +64,11 @@ class FuzzierUtilTest {
@Test
fun `Parse modules multiple modules and unique roots`() {
- val myFixture = testUtil.setUpMultiModuleProject(listOf("path/src1", "/path/src1/file1"), listOf("to/src2", "/to/src2/file2"), listOf("module/src3", "/module/src3/file3"))
+ val myFixture = testUtil.setUpMultiModuleProject(
+ listOf("path/src1", "/path/src1/file1"),
+ listOf("to/src2", "/to/src2/file2"),
+ listOf("module/src3", "/module/src3/file3")
+ )
fuzzierUtil.parseModules(myFixture.project)
val modules = myFixture.project.service().state.modules
@@ -78,7 +82,11 @@ class FuzzierUtilTest {
@Test
fun `Parse modules multiple modules with single root`() {
- val myFixture = testUtil.setUpMultiModuleProject(listOf("src1", "/src1/file1"), listOf("src1/module1", "/src1/module1/file1"), listOf("src1/module2", "/src1/module2/file1"))
+ val myFixture = testUtil.setUpMultiModuleProject(
+ listOf("src1", "/src1/file1"),
+ listOf("src1/module1", "/src1/module1/file1"),
+ listOf("src1/module2", "/src1/module2/file1")
+ )
fuzzierUtil.parseModules(myFixture.project)
val modules = myFixture.project.service().state.modules
@@ -91,8 +99,10 @@ class FuzzierUtilTest {
@Test
fun `Remove module paths, mixed set of modules`() {
- val myFixture = testUtil.setUpMultiModuleProject(listOf("src1", "/src1/file1"),
- listOf("src1/module1", "/src1/module1/file1"), listOf("src2", "/src2/file1"))
+ val myFixture = testUtil.setUpMultiModuleProject(
+ listOf("src1", "/src1/file1"),
+ listOf("src1/module1", "/src1/module1/file1"), listOf("src2", "/src2/file1")
+ )
val project = myFixture.project
fuzzierUtil.parseModules(project)
@@ -117,7 +127,11 @@ class FuzzierUtilTest {
@Test
fun `Remove module paths, include module dir on multi module project`() {
- val myFixture = testUtil.setUpMultiModuleProject(listOf("path/src1", "/path/src1/file1"), listOf("to/src2", "/to/src2/file2"), listOf("module/src3", "/module/src3/file3"))
+ val myFixture = testUtil.setUpMultiModuleProject(
+ listOf("path/src1", "/path/src1/file1"),
+ listOf("to/src2", "/to/src2/file2"),
+ listOf("module/src3", "/module/src3/file3")
+ )
val project = myFixture.project
fuzzierUtil.parseModules(project)
@@ -172,96 +186,6 @@ class FuzzierUtilTest {
assertEquals(1, modules.size)
}
- @Test
- fun `Sort and limit under limit`() {
- addElement(1, "file1")
- addElement(2, "file2")
- addElement(3, "file3")
-
- runWithLimit(5)
- assertEquals(3, result.size)
- assertEquals("file3", result[0].filename)
- assertEquals("file2", result[1].filename)
- assertEquals("file1", result[2].filename)
- }
-
- @Test
- fun `Sort and limit equal to limit`() {
- addElement(1, "file1")
- addElement(8, "file2")
- addElement(3, "file3")
-
- runWithLimit(3)
- assertEquals(3, result.size)
- assertEquals("file2", result[0].filename)
- assertEquals("file3", result[1].filename)
- assertEquals("file1", result[2].filename)
- }
-
- @Test
- fun `Sort and limit over limit`() {
- addElement(1, "file1")
- addElement(8, "file2")
- addElement(3, "file3")
- addElement(4, "file4")
-
- runWithLimit(2)
- assertEquals(2, result.size)
- assertEquals("file2", result[0].filename)
- assertEquals("file4", result[1].filename)
- }
-
- @Test
- fun `Empty list`() {
- runWithLimit(2)
- assertEquals(0, result.size)
- }
-
- @Test
- fun `Prioritize file paths with same score`() {
- addElement(0, "file1", "1")
- addElement(0, "file2", "123")
- addElement(0, "file3", "12")
- addElement(0, "file4", "1234")
-
- runPrioritizedList()
- assertEquals(4, result.size)
- assertEquals("file1", result[0].filename)
- assertEquals("file3", result[1].filename)
- assertEquals("file2", result[2].filename)
- assertEquals("file4", result[3].filename)
- }
-
- @Test
- fun `Prioritize file paths with different scores`() {
- addElement(10, "file1", "1")
- addElement(0, "file2", "123")
- addElement(0, "file3", "12")
- addElement(0, "file4", "1234")
-
- runPrioritizedList()
- assertEquals(4, result.size)
- assertEquals("file1", result[0].filename)
- assertEquals("file3", result[1].filename)
- assertEquals("file2", result[2].filename)
- assertEquals("file4", result[3].filename)
- }
-
- @Test
- fun `Prioritize empty paths`() {
- addElement(4, "file1", "")
- addElement(3, "file2", "")
- addElement(1, "file3", "")
- addElement(2, "file4", "")
-
- runPrioritizedList()
- assertEquals(4, result.size)
- assertEquals("file1", result[0].filename)
- assertEquals("file2", result[1].filename)
- assertEquals("file4", result[2].filename)
- assertEquals("file3", result[3].filename)
- }
-
@Test
fun `Test ignored characters`() {
val searchString = "HELLO/THERE/GENERAL/KENOBI"
@@ -275,42 +199,4 @@ class FuzzierUtilTest {
val ignoredChars = ""
assertEquals(searchString.lowercase(), FuzzierUtil.cleanSearchString(searchString, ignoredChars))
}
-
- private fun addElement(score: Int, fileName: String) {
- val fuzzyScore = FuzzyScore()
- fuzzyScore.streakScore = score
- val container = FuzzyMatchContainer(fuzzyScore, "", fileName, "")
- listModel.addElement(container)
- }
-
- private fun addElement(score: Int, filename: String, filePath: String) {
- val fuzzyScore = FuzzyScore()
- fuzzyScore.streakScore = score
- val container = FuzzyMatchContainer(fuzzyScore, filePath, filename, "")
- listModel.addElement(container)
- }
-
- private fun runPrioritizedList() {
- fuzzierUtil.setListLimit(4)
- fuzzierUtil.setPrioritizeShorterDirPaths(true)
- result = fuzzierUtil.sortAndLimit(listModel, true)
- }
-
- /**
- * Tests both the dir sort without priority and the file sorting
- */
- private fun runWithLimit(limit: Int) {
- fuzzierUtil.setListLimit(limit)
- fuzzierUtil.setPrioritizeShorterDirPaths(false)
- val dirResult = fuzzierUtil.sortAndLimit(listModel, true)
- result = fuzzierUtil.sortAndLimit(listModel)
-
- assertEquals(dirResult.size, result.size)
-
- var i = 0
- while (i < result.size) {
- assertEquals(result[i], dirResult[i])
- i++
- }
- }
}
\ No newline at end of file
diff --git a/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt b/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt
index ed13e6cc..5e83e23f 100644
--- a/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt
+++ b/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt
@@ -29,6 +29,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.FileType.FILE
import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService
import com.mituuz.fuzzier.settings.FuzzierSettingsService.State
@@ -151,7 +152,7 @@ class InitialViewHandlerTest {
val fuzzierSettingsServiceInstance: FuzzierSettingsService = service()
val fgss = service().state
val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "")
+ val container = FuzzyMatchContainer(score, "", "", "", FILE)
fuzzierSettingsServiceInstance.state.recentlySearchedFiles = null
InitialViewHandler.addFileToRecentlySearchedFiles(container, fuzzierSettingsServiceInstance.state, fgss)
@@ -165,11 +166,11 @@ class InitialViewHandlerTest {
val fgss = service().state
val fileListLimit = 2
val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "")
+ val container = FuzzyMatchContainer(score, "", "", "", FILE)
val largeList: DefaultListModel = DefaultListModel()
for (i in 0..25) {
- largeList.addElement(FuzzyMatchContainer(score, "" + i, "" + i, ""))
+ largeList.addElement(FuzzyMatchContainer(score, "" + i, "" + i, "", FILE))
}
fgss.fileListLimit = fileListLimit
@@ -189,11 +190,11 @@ class InitialViewHandlerTest {
val fgss = service().state
val fileListLimit = 20
val score = FuzzyMatchContainer.FuzzyScore()
- val container = FuzzyMatchContainer(score, "", "", "")
+ val container = FuzzyMatchContainer(score, "", "", "", FILE)
val largeList: DefaultListModel = DefaultListModel()
repeat(26) {
- largeList.addElement(FuzzyMatchContainer(score, "", "", ""))
+ largeList.addElement(FuzzyMatchContainer(score, "", "", "", FILE))
}
fgss.fileListLimit = fileListLimit