From 2e60f37fd861e1ce15f1857c33c166b1bbcc71c7 Mon Sep 17 00:00:00 2001 From: "mitja.leino" Date: Tue, 9 Dec 2025 19:53:31 +0200 Subject: [PATCH 01/61] Refactor unnecessary methods --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index e6ade29c..e8a474bd 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -207,30 +207,22 @@ open class Fuzzier : FuzzyAction() { listModel: DefaultListModel, task: Future<*>? ) { val moduleManager = ModuleManager.getInstance(project) + val filesToIterate = ConcurrentHashMap.newKeySet() + if (projectState.isProject) { - processProject(project, stringEvaluator, searchString, listModel, task) + FuzzierUtil.fileIndexToIterationFile( + filesToIterate, + ProjectFileIndex.getInstance(project), + project.name, + task + ) + processFiles(filesToIterate, stringEvaluator, listModel, searchString, task) } else { - processModules(moduleManager, stringEvaluator, searchString, listModel, task) + for (module in moduleManager.modules) { + FuzzierUtil.fileIndexToIterationFile(filesToIterate, module.rootManager.fileIndex, module.name, task) + } } - } - - 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) - } - 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) - } processFiles(filesToIterate, stringEvaluator, listModel, searchString, task) } From 8a36588c1ffdc9b5a5c503e273921e5660bfb2f3 Mon Sep 17 00:00:00 2001 From: "mitja.leino" Date: Tue, 9 Dec 2025 20:08:18 +0200 Subject: [PATCH 02/61] Refactor concurrent HashMap to a simple list --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 14 +++++++------- .../kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt | 12 +++++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index e8a474bd..6bfd2c9a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -207,30 +207,30 @@ open class Fuzzier : FuzzyAction() { listModel: DefaultListModel, task: Future<*>? ) { val moduleManager = ModuleManager.getInstance(project) - val filesToIterate = ConcurrentHashMap.newKeySet() + val iterationFiles = mutableListOf() if (projectState.isProject) { FuzzierUtil.fileIndexToIterationFile( - filesToIterate, + iterationFiles, ProjectFileIndex.getInstance(project), project.name, task ) - processFiles(filesToIterate, stringEvaluator, listModel, searchString, task) + processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) } else { for (module in moduleManager.modules) { - FuzzierUtil.fileIndexToIterationFile(filesToIterate, module.rootManager.fileIndex, module.name, task) + FuzzierUtil.fileIndexToIterationFile(iterationFiles, module.rootManager.fileIndex, module.name, task) } } - processFiles(filesToIterate, stringEvaluator, listModel, searchString, task) + processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) } /** * Processes a set of IterationFiles concurrently */ private fun processFiles( - filesToIterate: ConcurrentHashMap.KeySetView, + iterationFiles: List, stringEvaluator: StringEvaluator, listModel: DefaultListModel, searchString: String, task: Future<*>? ) { @@ -238,7 +238,7 @@ open class Fuzzier : FuzzyAction() { val processedFiles = ConcurrentHashMap.newKeySet() runBlocking { withContext(Dispatchers.IO) { - filesToIterate.forEach { iterationFile -> + iterationFiles.forEach { iterationFile -> if (task?.isCancelled == true) return@forEach if (processedFiles.add(iterationFile.file.path)) { launch { diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index 381a171e..a8f43e3a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -56,8 +56,18 @@ class FuzzierUtil { return "${baseDimensionKey}_${screenBounds.width}_${screenBounds.height}_${screenBounds.x}_${screenBounds.y}" } + /** + * Iterates through the content of a file index and adds matching files to the provided collection. + * This copies the file index state quickly to not block unnecessarily. + * + * @param iterationFiles a thread-safe `KeySetView` collection to hold the iteration results. + * @param fileIndex a `FileIndex` to iterate through its content. + * @param moduleName a string representing the name of the module associated with the files. + * @param task an optional `Future` instance to check for cancellation during iteration. + * @param isDir a boolean flag indicating whether to process directories (true) or files (false); defaults to false. + */ fun fileIndexToIterationFile( - iterationFiles: ConcurrentHashMap.KeySetView, + iterationFiles: MutableList, fileIndex: FileIndex, moduleName: String, task: Future<*>?, isDir: Boolean = false ) { From 9c25f06a1a6ff8456315e475e40a8a375a37ecd5 Mon Sep 17 00:00:00 2001 From: "mitja.leino" Date: Tue, 9 Dec 2025 20:19:45 +0200 Subject: [PATCH 03/61] Refactor list processing --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 6bfd2c9a..86fd727a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -206,20 +206,16 @@ open class Fuzzier : FuzzyAction() { project: Project, stringEvaluator: StringEvaluator, searchString: String, listModel: DefaultListModel, task: Future<*>? ) { - val moduleManager = ModuleManager.getInstance(project) - val iterationFiles = mutableListOf() - - if (projectState.isProject) { - FuzzierUtil.fileIndexToIterationFile( - iterationFiles, - ProjectFileIndex.getInstance(project), - project.name, - task - ) - processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) + val indexTargets = if (projectState.isProject) { + listOf(ProjectFileIndex.getInstance(project) to project.name) } else { - for (module in moduleManager.modules) { - FuzzierUtil.fileIndexToIterationFile(iterationFiles, module.rootManager.fileIndex, module.name, task) + val moduleManager = ModuleManager.getInstance(project) + moduleManager.modules.map { it.rootManager.fileIndex to it.name } + } + + val iterationFiles = buildList { + indexTargets.forEach { (fileIndex, moduleName) -> + FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, task) } } From 56617069a0f1a1d2d4e3461cc27105e641382739 Mon Sep 17 00:00:00 2001 From: "mitja.leino" Date: Tue, 9 Dec 2025 20:51:55 +0200 Subject: [PATCH 04/61] Separate methods to avoid passing parameters --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 86fd727a..27e83b4f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -152,15 +152,15 @@ open class Fuzzier : FuzzyAction() { var listModel = DefaultListModel() val stringEvaluator = getStringEvaluator() - if (task?.isCancelled == true) return@executeOnPooledThread - process(project, stringEvaluator, searchString, listModel, task) + val iterationFiles = collectIterationFiles(project, task) + if (task?.isCancelled == true) return@executeOnPooledThread + processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) if (task?.isCancelled == true) return@executeOnPooledThread listModel = fuzzierUtil.sortAndLimit(listModel) - if (task?.isCancelled == true) return@executeOnPooledThread ApplicationManager.getApplication().invokeLater { @@ -202,10 +202,7 @@ open class Fuzzier : FuzzyAction() { ) } - private fun process( - project: Project, stringEvaluator: StringEvaluator, searchString: String, - listModel: DefaultListModel, task: Future<*>? - ) { + private fun collectIterationFiles(project: Project, task: Future<*>?): List { val indexTargets = if (projectState.isProject) { listOf(ProjectFileIndex.getInstance(project) to project.name) } else { @@ -213,13 +210,11 @@ open class Fuzzier : FuzzyAction() { moduleManager.modules.map { it.rootManager.fileIndex to it.name } } - val iterationFiles = buildList { + return buildList { indexTargets.forEach { (fileIndex, moduleName) -> FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, task) } } - - processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) } /** From 2d24765d4d5fe905822feac2eda425c1b40566a5 Mon Sep 17 00:00:00 2001 From: "mitja.leino" Date: Thu, 11 Dec 2025 06:51:27 +0200 Subject: [PATCH 05/61] Dont use listmodel --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 7 ++++++- .../mituuz/fuzzier/entities/StringEvaluator.kt | 16 +++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 27e83b4f..d53f59aa 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -57,6 +57,7 @@ import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Future import javax.swing.AbstractAction import javax.swing.DefaultListModel @@ -227,18 +228,22 @@ open class Fuzzier : FuzzyAction() { ) { val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) val processedFiles = ConcurrentHashMap.newKeySet() + val results = ConcurrentLinkedQueue() runBlocking { withContext(Dispatchers.IO) { iterationFiles.forEach { iterationFile -> if (task?.isCancelled == true) return@forEach if (processedFiles.add(iterationFile.file.path)) { launch { - stringEvaluator.evaluateFile(iterationFile, listModel, ss) + val container = stringEvaluator.evaluateFile(iterationFile, ss) + container?.let { results.add(it) } } } } } } + + results.forEach { listModel.addElement(it) } } private fun openFile(project: Project, fuzzyContainer: FuzzyContainer?, virtualFile: VirtualFile) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 6962dc65..0769ba71 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -95,27 +95,25 @@ class StringEvaluator( } } - fun evaluateFile( - iterationFile: FuzzierUtil.IterationFile, listModel: DefaultListModel, - searchString: String - ) { + fun evaluateFile(iterationFile: FuzzierUtil.IterationFile, searchString: String): FuzzyMatchContainer? { val scoreCalculator = ScoreCalculator(searchString) val file = iterationFile.file val moduleName = iterationFile.module + if (!file.isDirectory) { - val moduleBasePath = modules[moduleName] ?: return + val moduleBasePath = modules[moduleName] ?: return null val filePath = file.path.removePrefix(moduleBasePath) if (isExcluded(file, filePath)) { - return + return null } if (filePath.isNotBlank()) { val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator) - if (fuzzyMatchContainer != null) { - listModel.addElement(fuzzyMatchContainer) - } + return fuzzyMatchContainer } } + + return null } private fun getDirPath(virtualFile: VirtualFile, basePath: String, module: String): String { From 72e06183308df87d491192cff84717bb167e976f Mon Sep 17 00:00:00 2001 From: Mitja Date: Thu, 11 Dec 2025 19:10:51 +0200 Subject: [PATCH 06/61] Combine processing and list handling --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index d53f59aa..93476dbd 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -43,6 +43,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer +import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.* import com.mituuz.fuzzier.util.FuzzierUtil @@ -56,8 +57,8 @@ import java.awt.event.ActionEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import java.util.* import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Future import javax.swing.AbstractAction import javax.swing.DefaultListModel @@ -150,7 +151,6 @@ open class Fuzzier : FuzzyAction() { // 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 @@ -158,10 +158,7 @@ open class Fuzzier : FuzzyAction() { val iterationFiles = collectIterationFiles(project, task) if (task?.isCancelled == true) return@executeOnPooledThread - processFiles(iterationFiles, stringEvaluator, listModel, searchString, task) - if (task?.isCancelled == true) return@executeOnPooledThread - - listModel = fuzzierUtil.sortAndLimit(listModel) + val listModel = processFiles(iterationFiles, stringEvaluator, searchString, task) if (task?.isCancelled == true) return@executeOnPooledThread ApplicationManager.getApplication().invokeLater { @@ -223,12 +220,19 @@ open class Fuzzier : FuzzyAction() { */ private fun processFiles( iterationFiles: List, - stringEvaluator: StringEvaluator, listModel: DefaultListModel, + stringEvaluator: StringEvaluator, searchString: String, task: Future<*>? - ) { + ): DefaultListModel { val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) val processedFiles = ConcurrentHashMap.newKeySet() - val results = ConcurrentLinkedQueue() + val listLimit = globalState.fileListLimit + val priorityQueue = PriorityQueue( + listLimit + 1, + compareBy { it.getScore() } + ) + + var minimumScore: Int? = null + runBlocking { withContext(Dispatchers.IO) { iterationFiles.forEach { iterationFile -> @@ -236,14 +240,39 @@ open class Fuzzier : FuzzyAction() { if (processedFiles.add(iterationFile.file.path)) { launch { val container = stringEvaluator.evaluateFile(iterationFile, ss) - container?.let { results.add(it) } + container?.let { fuzzyMatchContainer -> + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + } } } } } } - results.forEach { listModel.addElement(it) } + + val result = DefaultListModel() + result.addAll( + priorityQueue.sortedWith( + compareByDescending { it.getScore() }) + ) + return result + } + + private fun PriorityQueue.maybeAdd( + minimumScore: Int?, + fuzzyMatchContainer: FuzzyMatchContainer + ): Int? { + var ret = minimumScore + + if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + this.add(fuzzyMatchContainer) + if (this.size > globalState.fileListLimit) { + this.remove() + ret = this.peek().getScore() + } + } + + return ret } private fun openFile(project: Project, fuzzyContainer: FuzzyContainer?, virtualFile: VirtualFile) { From 8e411f1122b65af93fad9f2e6895bd4b642f30c1 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 12 Dec 2025 08:32:19 +0200 Subject: [PATCH 07/61] Correct javadoc --- .../com/mituuz/fuzzier/util/FuzzierUtil.kt | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index a8f43e3a..3b54346c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -1,52 +1,51 @@ /* -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.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService +import com.mituuz.fuzzier.settings.FuzzierSettingsService import java.awt.Rectangle -import java.util.concurrent.ConcurrentHashMap +import java.util.* import java.util.concurrent.Future +import javax.swing.DefaultListModel 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 @@ -60,7 +59,7 @@ class FuzzierUtil { * Iterates through the content of a file index and adds matching files to the provided collection. * This copies the file index state quickly to not block unnecessarily. * - * @param iterationFiles a thread-safe `KeySetView` collection to hold the iteration results. + * @param iterationFiles a mutable list to hold the iteration files * @param fileIndex a `FileIndex` to iterate through its content. * @param moduleName a string representing the name of the module associated with the files. * @param task an optional `Future` instance to check for cancellation during iteration. From 309a73a2e0795f7dce70009ef6b7ddb2e79bf3e3 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 12 Dec 2025 16:23:09 +0200 Subject: [PATCH 08/61] Sync priority queue modifications --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 93476dbd..8fc02dd3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -231,6 +231,7 @@ open class Fuzzier : FuzzyAction() { compareBy { it.getScore() } ) + val queueLock = Any() var minimumScore: Int? = null runBlocking { @@ -241,7 +242,9 @@ open class Fuzzier : FuzzyAction() { launch { val container = stringEvaluator.evaluateFile(iterationFile, ss) container?.let { fuzzyMatchContainer -> - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + } } } } From a8282217a5256dd97a86f0e322a66044da9bc803 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 12 Dec 2025 17:40:16 +0200 Subject: [PATCH 09/61] Ensure all files are processed before handling the results --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 8fc02dd3..b6a5a2d3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -51,7 +51,6 @@ 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 @@ -234,17 +233,17 @@ open class Fuzzier : FuzzyAction() { val queueLock = Any() var minimumScore: Int? = null - runBlocking { - withContext(Dispatchers.IO) { + runBlocking(Dispatchers.IO) { + kotlinx.coroutines.coroutineScope { iterationFiles.forEach { iterationFile -> if (task?.isCancelled == true) return@forEach - if (processedFiles.add(iterationFile.file.path)) { - launch { - val container = stringEvaluator.evaluateFile(iterationFile, ss) - container?.let { fuzzyMatchContainer -> - synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) - } + if (!processedFiles.add(iterationFile.file.path)) return@forEach + + launch { + val container = stringEvaluator.evaluateFile(iterationFile, ss) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) } } } From 88151f89d1ea55ed30d9c26b1e949a5c0879e141 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 09:15:01 +0200 Subject: [PATCH 10/61] Update javadoc --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index b6a5a2d3..d9df5b36 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -216,6 +216,7 @@ open class Fuzzier : FuzzyAction() { /** * Processes a set of IterationFiles concurrently + * @return a priority list which has been size limited and sorted */ private fun processFiles( iterationFiles: List, From 5780f7a5d9ed0c248e06aa93002d29f5444c8020 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 09:31:29 +0200 Subject: [PATCH 11/61] Move common functionality to component, use non nullable list --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 46 +++++++------------ .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 7 +-- .../fuzzier/components/FuzzyComponent.kt | 13 +++++- .../components/FuzzyFinderComponent.kt | 2 +- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index d9df5b36..3f8414e6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -25,6 +25,7 @@ package com.mituuz.fuzzier import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.EditorHistoryManager @@ -48,9 +49,7 @@ import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.* import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.apache.commons.lang3.StringUtils import java.awt.event.ActionEvent import java.awt.event.KeyEvent @@ -63,11 +62,12 @@ 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" + private var currentUpdateListContentJob: Job? = null + private var actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) // Used by FuzzierVCS to check if files are tracked by the VCS protected var changeListManager: ChangeListManager? = null @@ -144,35 +144,23 @@ open class Fuzzier : FuzzyAction() { 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) + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) { + // Create a reference to the current task to check if it has been cancelled + val task = currentTask + component.fileList.setPaintBusy(true) - val stringEvaluator = getStringEvaluator() - if (task?.isCancelled == true) return@executeOnPooledThread + val stringEvaluator = getStringEvaluator() + if (task?.isCancelled == true) return@launch - val iterationFiles = collectIterationFiles(project, task) - if (task?.isCancelled == true) return@executeOnPooledThread + val iterationFiles = collectIterationFiles(project, task) + if (task?.isCancelled == true) return@launch - val listModel = processFiles(iterationFiles, stringEvaluator, searchString, task) - if (task?.isCancelled == true) return@executeOnPooledThread + val listModel = processFiles(iterationFiles, stringEvaluator, searchString, task) + if (task?.isCancelled == true) return@launch - 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 - } + // Does this need to still happen in an invokeLater block? + component.refreshModel(listModel, getCellRenderer()) } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index c9cc3a34..76fd03f1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -216,12 +216,7 @@ open class FuzzyGrep() : FuzzyAction() { if (currentJob?.isCancelled == true) return@launch - component.fileList.model = results - component.fileList.cellRenderer = getCellRenderer() - if (!results.isEmpty) { - component.fileList.selectedIndex = 0 - } - component.fileList.setPaintBusy(false) + component.refreshModel(results, getCellRenderer()) } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt index fe6e39c4..49eb2016 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt @@ -28,9 +28,20 @@ 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 + } + fileList.setPaintBusy(false) + } } \ 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) From da246acac92d8c77c5f053927ad5ca3b5e6c51dc Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 10:14:52 +0200 Subject: [PATCH 12/61] Use coroutine --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 54 ++++++++++++------- .../com/mituuz/fuzzier/util/FuzzierUtil.kt | 16 ++++-- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 3f8414e6..716f68bf 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -50,6 +50,7 @@ import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode. import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import org.apache.commons.lang3.StringUtils import java.awt.event.ActionEvent import java.awt.event.KeyEvent @@ -57,7 +58,6 @@ import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.* import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Future import javax.swing.AbstractAction import javax.swing.DefaultListModel import javax.swing.JComponent @@ -147,17 +147,20 @@ open class Fuzzier : FuzzyAction() { currentUpdateListContentJob?.cancel() currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) { // Create a reference to the current task to check if it has been cancelled - val task = currentTask component.fileList.setPaintBusy(true) val stringEvaluator = getStringEvaluator() - if (task?.isCancelled == true) return@launch + coroutineContext.ensureActive() - val iterationFiles = collectIterationFiles(project, task) - if (task?.isCancelled == true) return@launch + val iterationFiles = withContext(Dispatchers.Default) { + collectIterationFiles(project) + } + coroutineContext.ensureActive() - val listModel = processFiles(iterationFiles, stringEvaluator, searchString, task) - if (task?.isCancelled == true) return@launch + val listModel = withContext(Dispatchers.Default) { + processFiles(iterationFiles, stringEvaluator, searchString) + } + coroutineContext.ensureActive() // Does this need to still happen in an invokeLater block? component.refreshModel(listModel, getCellRenderer()) @@ -187,7 +190,7 @@ open class Fuzzier : FuzzyAction() { ) } - private fun collectIterationFiles(project: Project, task: Future<*>?): List { + private suspend fun collectIterationFiles(project: Project): List { val indexTargets = if (projectState.isProject) { listOf(ProjectFileIndex.getInstance(project) to project.name) } else { @@ -197,7 +200,8 @@ open class Fuzzier : FuzzyAction() { return buildList { indexTargets.forEach { (fileIndex, moduleName) -> - FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, task) + currentCoroutineContext().ensureActive() + FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, currentCoroutineContext().job) } } } @@ -206,10 +210,10 @@ open class Fuzzier : FuzzyAction() { * Processes a set of IterationFiles concurrently * @return a priority list which has been size limited and sorted */ - private fun processFiles( + private suspend fun processFiles( iterationFiles: List, stringEvaluator: StringEvaluator, - searchString: String, task: Future<*>? + searchString: String ): DefaultListModel { val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) val processedFiles = ConcurrentHashMap.newKeySet() @@ -222,21 +226,31 @@ open class Fuzzier : FuzzyAction() { val queueLock = Any() var minimumScore: Int? = null - runBlocking(Dispatchers.IO) { - kotlinx.coroutines.coroutineScope { - iterationFiles.forEach { iterationFile -> - if (task?.isCancelled == true) return@forEach - if (!processedFiles.add(iterationFile.file.path)) return@forEach + val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) + val parallelism = (cores - 1).coerceIn(1, 8) + + withContext(Dispatchers.Default) { + coroutineScope { + val ch = Channel(capacity = parallelism * 2) + repeat(parallelism) { launch { - val container = stringEvaluator.evaluateFile(iterationFile, ss) - container?.let { fuzzyMatchContainer -> - synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + for (iterationFile in ch) { + val container = stringEvaluator.evaluateFile(iterationFile, ss) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + } } } } } + + for (iterationFile in iterationFiles) { + if (!processedFiles.add(iterationFile.file.path)) continue + ch.send(iterationFile) + } + ch.close() } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index 3b54346c..b57cd879 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -23,6 +23,7 @@ */ package com.mituuz.fuzzier.util +import com.intellij.ide.plugins.newui.OneLineProgressIndicator.task import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager @@ -34,10 +35,12 @@ import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService +import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive import java.awt.Rectangle import java.util.* -import java.util.concurrent.Future import javax.swing.DefaultListModel +import kotlin.coroutines.cancellation.CancellationException class FuzzierUtil { private var globalState = service().state @@ -67,13 +70,18 @@ class FuzzierUtil { */ fun fileIndexToIterationFile( iterationFiles: MutableList, - fileIndex: FileIndex, moduleName: String, task: Future<*>?, - isDir: Boolean = false + fileIndex: FileIndex, + moduleName: String, + job: Job, + isDir: Boolean = false, ) { fileIndex.iterateContent { file -> - if (task?.isCancelled == true) { + try { + job.ensureActive() + } catch (_: CancellationException) { return@iterateContent false } + if (file.isDirectory == isDir) { iterationFiles.add(IterationFile(file, moduleName)) } From 30281ab6291db4de87020db2b31090d3cd8d21b0 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 10:27:45 +0200 Subject: [PATCH 13/61] Cleanup, handle busy paint on a finally block --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 34 +++++++++++-------- .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 26 ++++++++------ .../fuzzier/components/FuzzyComponent.kt | 1 - 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 716f68bf..f9584a0e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -149,21 +149,24 @@ open class Fuzzier : FuzzyAction() { // Create a reference to the current task to check if it has been cancelled component.fileList.setPaintBusy(true) - val stringEvaluator = getStringEvaluator() - coroutineContext.ensureActive() + try { + val stringEvaluator = getStringEvaluator() + coroutineContext.ensureActive() - val iterationFiles = withContext(Dispatchers.Default) { - collectIterationFiles(project) - } - coroutineContext.ensureActive() + val iterationFiles = withContext(Dispatchers.Default) { + collectIterationFiles(project) + } + coroutineContext.ensureActive() - val listModel = withContext(Dispatchers.Default) { - processFiles(iterationFiles, stringEvaluator, searchString) - } - coroutineContext.ensureActive() + val listModel = withContext(Dispatchers.Default) { + processFiles(iterationFiles, stringEvaluator, searchString) + } + coroutineContext.ensureActive() - // Does this need to still happen in an invokeLater block? - component.refreshModel(listModel, getCellRenderer()) + component.refreshModel(listModel, getCellRenderer()) + } finally { + component.fileList.setPaintBusy(false) + } } } @@ -191,6 +194,9 @@ open class Fuzzier : FuzzyAction() { } private 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 { @@ -200,8 +206,8 @@ open class Fuzzier : FuzzyAction() { return buildList { indexTargets.forEach { (fileIndex, moduleName) -> - currentCoroutineContext().ensureActive() - FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, currentCoroutineContext().job) + ctx.ensureActive() + FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, job) } } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 76fd03f1..c04488b0 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -200,23 +200,27 @@ open class FuzzyGrep() : FuzzyAction() { currentUpdateListContentJob?.cancel() currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) { - val currentJob = currentUpdateListContentJob + component.fileList.setPaintBusy(true) + try { + val currentJob = currentUpdateListContentJob - if (currentJob?.isCancelled == true) return@launch + if (currentJob?.isCancelled == true) return@launch - component.fileList.setPaintBusy(true) - val listModel = DefaultListModel() + val listModel = DefaultListModel() - if (currentJob?.isCancelled == true) return@launch + if (currentJob?.isCancelled == true) return@launch - val results = withContext(Dispatchers.IO) { - findInFiles(searchString, listModel, project.basePath.toString()) - listModel - } + val results = withContext(Dispatchers.IO) { + findInFiles(searchString, listModel, project.basePath.toString()) + listModel + } - if (currentJob?.isCancelled == true) return@launch + if (currentJob?.isCancelled == true) return@launch - component.refreshModel(results, getCellRenderer()) + component.refreshModel(results, getCellRenderer()) + } finally { + component.fileList.setPaintBusy(false) + } } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt index 49eb2016..71de5404 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt @@ -42,6 +42,5 @@ open class FuzzyComponent : JPanel() { if (!fileList.isEmpty) { fileList.selectedIndex = 0 } - fileList.setPaintBusy(false) } } \ No newline at end of file From f6877a7eb17e5be11993ea3f4c7d541e121ab737 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 10:29:15 +0200 Subject: [PATCH 14/61] Remove unnecessay context switch --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index f9584a0e..615878c8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -42,6 +42,7 @@ 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.platform.ide.progress.ModalTaskOwner.component import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer @@ -235,29 +236,27 @@ open class Fuzzier : FuzzyAction() { val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) val parallelism = (cores - 1).coerceIn(1, 8) - withContext(Dispatchers.Default) { - coroutineScope { - val ch = Channel(capacity = parallelism * 2) - - repeat(parallelism) { - launch { - for (iterationFile in ch) { - val container = stringEvaluator.evaluateFile(iterationFile, ss) - container?.let { fuzzyMatchContainer -> - synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) - } + coroutineScope { + val ch = Channel(capacity = parallelism * 2) + + repeat(parallelism) { + launch { + for (iterationFile in ch) { + val container = stringEvaluator.evaluateFile(iterationFile, ss) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) } } } } + } - for (iterationFile in iterationFiles) { - if (!processedFiles.add(iterationFile.file.path)) continue - ch.send(iterationFile) - } - ch.close() + for (iterationFile in iterationFiles) { + if (!processedFiles.add(iterationFile.file.path)) continue + ch.send(iterationFile) } + ch.close() } From 02d7b885eacf337eed88e995e3cd6a6dc5fee64c Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 10:35:57 +0200 Subject: [PATCH 15/61] CLeanup cancellation check --- .../kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index b57cd879..ea6fb66e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -23,7 +23,6 @@ */ package com.mituuz.fuzzier.util -import com.intellij.ide.plugins.newui.OneLineProgressIndicator.task import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager @@ -36,11 +35,9 @@ import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import kotlinx.coroutines.Job -import kotlinx.coroutines.ensureActive import java.awt.Rectangle import java.util.* import javax.swing.DefaultListModel -import kotlin.coroutines.cancellation.CancellationException class FuzzierUtil { private var globalState = service().state @@ -65,7 +62,7 @@ class FuzzierUtil { * @param iterationFiles a mutable list to hold the iteration files * @param fileIndex a `FileIndex` to iterate through its content. * @param moduleName a string representing the name of the module associated with the files. - * @param task an optional `Future` instance to check for cancellation during iteration. + * @param job current coroutine context, which can be used to stop the iteration * @param isDir a boolean flag indicating whether to process directories (true) or files (false); defaults to false. */ fun fileIndexToIterationFile( @@ -76,11 +73,7 @@ class FuzzierUtil { isDir: Boolean = false, ) { fileIndex.iterateContent { file -> - try { - job.ensureActive() - } catch (_: CancellationException) { - return@iterateContent false - } + if (!job.isActive) return@iterateContent false if (file.isDirectory == isDir) { iterationFiles.add(IterationFile(file, moduleName)) From 04ff31e316a9cd3b4584a2329b10be47d581507f Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 10:45:15 +0200 Subject: [PATCH 16/61] Cleanup and add explicit close to popup --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 8 ++++++-- src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 615878c8..2f69474c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -42,7 +42,6 @@ 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.platform.ide.progress.ModalTaskOwner.component import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer @@ -99,8 +98,13 @@ open class Fuzzier : FuzzyAction() { override fun onClosed(event: LightweightWindowEvent) { globalState.splitPosition = (component as FuzzyFinderComponent).splitPane.dividerLocation + resetOriginalHandlers() - super.onClosed(event) + + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = null + + actionScope.cancel() } }) diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index c04488b0..7816e904 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -166,10 +166,16 @@ open class FuzzyGrep() : FuzzyAction() { override fun onClosed(event: LightweightWindowEvent) { globalState.splitPosition = (component as FuzzyFinderComponent).splitPane.dividerLocation + resetOriginalHandlers() - super.onClosed(event) + currentLaunchJob?.cancel() + currentLaunchJob = null + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = null + + actionScope.cancel() } }) From bc288583f26982688273e7dbc75b494f09547e7a Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 11:03:31 +0200 Subject: [PATCH 17/61] Correctly reinstate action scope --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 9 ++++++--- src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 2f69474c..5d35df91 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -67,7 +67,7 @@ open class Fuzzier : FuzzyAction() { override var popupTitle = "Fuzzy Search" override var dimensionKey = "FuzzySearchPopup" private var currentUpdateListContentJob: Job? = null - private var actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var actionScope: CoroutineScope? = null // Used by FuzzierVCS to check if files are tracked by the VCS protected var changeListManager: ChangeListManager? = null @@ -75,6 +75,9 @@ open class Fuzzier : FuzzyAction() { override fun runAction(project: Project, actionEvent: AnActionEvent) { setCustomHandlers() + actionScope?.cancel() + actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + ApplicationManager.getApplication().invokeLater { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project) @@ -104,7 +107,7 @@ open class Fuzzier : FuzzyAction() { currentUpdateListContentJob?.cancel() currentUpdateListContentJob = null - actionScope.cancel() + actionScope?.cancel() } }) @@ -150,7 +153,7 @@ open class Fuzzier : FuzzyAction() { } currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) { + currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { // Create a reference to the current task to check if it has been cancelled component.fileList.setPaintBusy(true) diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 7816e904..812db942 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -78,7 +78,7 @@ open class FuzzyGrep() : FuzzyAction() { 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) + private var actionScope: CoroutineScope? = null override fun runAction( project: Project, @@ -87,8 +87,11 @@ open class FuzzyGrep() : FuzzyAction() { currentLaunchJob?.cancel() setCustomHandlers() + actionScope?.cancel() + actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val projectBasePath = project.basePath.toString() - currentLaunchJob = actionScope.launch(Dispatchers.EDT) { + currentLaunchJob = actionScope?.launch(Dispatchers.EDT) { val currentJob = currentLaunchJob if (!isInstalled("rg", projectBasePath)) { @@ -175,7 +178,7 @@ open class FuzzyGrep() : FuzzyAction() { currentUpdateListContentJob?.cancel() currentUpdateListContentJob = null - actionScope.cancel() + actionScope?.cancel() } }) @@ -205,7 +208,7 @@ open class FuzzyGrep() : FuzzyAction() { } currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = actionScope.launch(Dispatchers.EDT) { + currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { component.fileList.setPaintBusy(true) try { val currentJob = currentUpdateListContentJob From c7e38a3fa36cde71ae4f374195d893969ffd3e3e Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 11:59:08 +0200 Subject: [PATCH 18/61] Use SingleAlarm to handle preview updates and introduce debouncing --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 98 ++++++++++--------- .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 4 +- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 5d35df91..f302e482 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -30,9 +30,6 @@ 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 @@ -42,6 +39,7 @@ 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.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer @@ -68,6 +66,8 @@ open class Fuzzier : FuzzyAction() { override var dimensionKey = "FuzzySearchPopup" private var currentUpdateListContentJob: Job? = null private var actionScope: CoroutineScope? = null + private var previewAlarm: SingleAlarm? = null + private var lastPreviewKey: String? = null // Used by FuzzierVCS to check if files are tracked by the VCS protected var changeListManager: ChangeListManager? = null @@ -81,6 +81,7 @@ open class Fuzzier : FuzzyAction() { ApplicationManager.getApplication().invokeLater { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project) + previewAlarm = getPreviewAlarm() createListeners(project) showPopup(project) createSharedListeners(project) @@ -108,40 +109,40 @@ open class Fuzzier : FuzzyAction() { currentUpdateListContentJob = null actionScope?.cancel() + + previewAlarm?.dispose() + lastPreviewKey = null } }) return popup } - /** - * Populates the file list with recently opened files - */ private fun createInitialView(project: Project) { + component.fileList.setPaintBusy(true) 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() + try { + 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) + ApplicationManager.getApplication().invokeLater { + component.refreshModel(listModel, getCellRenderer()) } + } finally { + component.fileList.setPaintBusy(false) } } } @@ -318,24 +319,10 @@ open class Fuzzier : FuzzyAction() { 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()}" - - 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) - } - } - }) + if (event.valueIsAdjusting) { + return@addListSelectionListener + } else { + previewAlarm?.cancelAndRequest() } } @@ -370,4 +357,27 @@ open class Fuzzier : FuzzyAction() { } }) } + + private fun getPreviewAlarm(): SingleAlarm { + return SingleAlarm( + { + val fuzzyFinderComponent = (component as FuzzyFinderComponent) + val selected = component.fileList.selectedValue + + if (selected == null || component.fileList.isEmpty) { + defaultDoc?.let { fuzzyFinderComponent.previewPane.updateFile(it) } + lastPreviewKey = null + return@SingleAlarm + } + + 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/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 812db942..33ee1457 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -341,7 +341,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 @@ -349,7 +349,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) } From 1de9a84e9f4698b7a566c7c5c32aadc830860731 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 15:12:52 +0200 Subject: [PATCH 19/61] Create and test a separate collector --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 14 +- .../mituuz/fuzzier/entities/IterationFile.kt | 27 ++++ .../IntelliJIterationFileCollector.kt | 45 ++++++ .../iteration/IterationFileCollector.kt | 35 +++++ .../IntelliJIterationFileCollectorTest.kt | 141 ++++++++++++++++++ 5 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt create mode 100644 src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt create mode 100644 src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt create mode 100644 src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index f302e482..1962cd16 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -44,6 +44,8 @@ import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.entities.StringEvaluator +import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector +import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.* import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler @@ -68,6 +70,7 @@ open class Fuzzier : FuzzyAction() { private var actionScope: CoroutineScope? = null private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null + private var collector: IterationFileCollector = IntelliJIterationFileCollector() // Used by FuzzierVCS to check if files are tracked by the VCS protected var changeListManager: ChangeListManager? = null @@ -213,12 +216,7 @@ open class Fuzzier : FuzzyAction() { moduleManager.modules.map { it.rootManager.fileIndex to it.name } } - return buildList { - indexTargets.forEach { (fileIndex, moduleName) -> - ctx.ensureActive() - FuzzierUtil.fileIndexToIterationFile(this, fileIndex, moduleName, job) - } - } + return collector.collectFiles(indexTargets, shouldContinue = { job.isActive }) } /** @@ -380,4 +378,8 @@ open class Fuzzier : FuzzyAction() { 75, ) } + + fun setCollector(iterationFileCollector: IterationFileCollector) { + this.collector = iterationFileCollector + } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt new file mode 100644 index 00000000..7b50bb89 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt @@ -0,0 +1,27 @@ +/* + * 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 IterationFile() 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..f6b8c22d --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -0,0 +1,45 @@ +/* + * 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.mituuz.fuzzier.util.FuzzierUtil + +class IntelliJIterationFileCollector : IterationFileCollector { + override fun collectFiles( + targets: List>, + shouldContinue: () -> Boolean, + ): List = buildList { + for ((fileIndex, moduleName) in targets) { + fileIndex.iterateContent { vf -> + if (!shouldContinue()) return@iterateContent false + if (!vf.isDirectory) { + add(FuzzierUtil.IterationFile(vf, moduleName)) + } + 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..9723fdaf --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -0,0 +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. + */ + +package com.mituuz.fuzzier.intellij.iteration + +import com.intellij.openapi.roots.FileIndex +import com.mituuz.fuzzier.util.FuzzierUtil + +interface IterationFileCollector { + fun collectFiles( + targets: List>, + shouldContinue: () -> Boolean, + ): List +} \ No newline at end of file diff --git a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt new file mode 100644 index 00000000..44ff4d12 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt @@ -0,0 +1,141 @@ +/* + * 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 IntelliJIterationFileCollectorTest { + private lateinit var collector: IntelliJIterationFileCollector + + @BeforeEach + fun setUp() { + collector = IntelliJIterationFileCollector() + } + + @Test + fun `collectFiles returns empty list when targets are empty`() { + val result = collector.collectFiles(emptyList()) { 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 } // true for first file, false after + ) + + assertEquals(1, res.size) + assertEquals("a.txt", res[0].file.name) + } + + @Test + fun `skips directories`() { + 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 } + ) + + assertEquals(2, res.size) + assertEquals("a.txt", res[0].file.name) + assertEquals("b.txt", res[1].file.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 } + ) + + assertEquals(4, res.size) + assertEquals("a.txt", res[0].file.name) + assertEquals("mod1", res[0].module) + assertEquals("b.txt", res[1].file.name) + assertEquals("mod1", res[1].module) + assertEquals("c.txt", res[2].file.name) + assertEquals("mod2", res[2].module) + assertEquals("d.txt", res[3].file.name) + assertEquals("mod2", res[3].module) + } +} \ No newline at end of file From 7f3fd4704240cb3a40f381b0c28350afe7135fad Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 15:19:56 +0200 Subject: [PATCH 20/61] Remove unused function and class --- .../mituuz/fuzzier/entities/IterationFile.kt | 27 ----------------- .../com/mituuz/fuzzier/util/FuzzierUtil.kt | 29 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt deleted file mode 100644 index 7b50bb89..00000000 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFile.kt +++ /dev/null @@ -1,27 +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.entities - -data class IterationFile() diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index ea6fb66e..efb1229e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -28,13 +28,11 @@ 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.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService -import kotlinx.coroutines.Job import java.awt.Rectangle import java.util.* import javax.swing.DefaultListModel @@ -55,33 +53,6 @@ class FuzzierUtil { return "${baseDimensionKey}_${screenBounds.width}_${screenBounds.height}_${screenBounds.x}_${screenBounds.y}" } - /** - * Iterates through the content of a file index and adds matching files to the provided collection. - * This copies the file index state quickly to not block unnecessarily. - * - * @param iterationFiles a mutable list to hold the iteration files - * @param fileIndex a `FileIndex` to iterate through its content. - * @param moduleName a string representing the name of the module associated with the files. - * @param job current coroutine context, which can be used to stop the iteration - * @param isDir a boolean flag indicating whether to process directories (true) or files (false); defaults to false. - */ - fun fileIndexToIterationFile( - iterationFiles: MutableList, - fileIndex: FileIndex, - moduleName: String, - job: Job, - isDir: Boolean = false, - ) { - fileIndex.iterateContent { file -> - if (!job.isActive) 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()) { From 9718dab1b04ab2d8b0bcb52d5128f99c7cd84d61 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 15:54:02 +0200 Subject: [PATCH 21/61] Create a file filter to avoid doing checks in multiple places --- asd.sql | 0 src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 14 ++--- .../kotlin/com/mituuz/fuzzier/FuzzierVCS.kt | 52 ++++++++++--------- .../fuzzier/entities/StringEvaluator.kt | 49 ++++++++--------- .../IntelliJIterationFileCollector.kt | 6 +-- .../iteration/IterationFileCollector.kt | 2 + 6 files changed, 62 insertions(+), 61 deletions(-) create mode 100644 asd.sql diff --git a/asd.sql b/asd.sql new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 1962cd16..5a33c24f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -36,7 +36,6 @@ 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 @@ -72,9 +71,6 @@ open class Fuzzier : FuzzyAction() { private var lastPreviewKey: String? = null private var collector: IterationFileCollector = IntelliJIterationFileCollector() - // Used by FuzzierVCS to check if files are tracked by the VCS - protected var changeListManager: ChangeListManager? = null - override fun runAction(project: Project, actionEvent: AnActionEvent) { setCustomHandlers() @@ -201,7 +197,6 @@ open class Fuzzier : FuzzyAction() { return StringEvaluator( combinedExclusions, projectState.modules, - changeListManager ) } @@ -216,9 +211,16 @@ open class Fuzzier : FuzzyAction() { moduleManager.modules.map { it.rootManager.fileIndex to it.name } } - return collector.collectFiles(indexTargets, shouldContinue = { job.isActive }) + return collector.collectFiles( + targets = indexTargets, + shouldContinue = { job.isActive }, + fileFilter = buildFileFilter(project) + ) } + protected open fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = + { vf -> !vf.isDirectory } + /** * Processes a set of IterationFiles concurrently * @return a priority list which has been size limited and sorted diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt index 5ee15ee7..6720f6f8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt @@ -1,38 +1,40 @@ /* -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.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/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 0769ba71..e6a2e7c4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -1,30 +1,29 @@ /* -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 @@ -38,7 +37,6 @@ import javax.swing.DefaultListModel class StringEvaluator( private var exclusionList: Set, private var modules: Map, - private var changeListManager: ChangeListManager? = null ) { lateinit var scoreCalculator: ScoreCalculator @@ -141,9 +139,6 @@ class StringEvaluator( * @return true if file should be excluded */ private fun isExcluded(file: VirtualFile, filePath: String): Boolean { - if (changeListManager !== null) { - return changeListManager!!.isIgnoredFile(file) - } return exclusionList.any { e -> when { e.startsWith("*") -> filePath.endsWith(e.substring(1)) diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt index f6b8c22d..9a4787d3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -25,19 +25,19 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.util.FuzzierUtil 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 (!vf.isDirectory) { - add(FuzzierUtil.IterationFile(vf, moduleName)) - } + if (fileFilter(vf)) add(FuzzierUtil.IterationFile(vf, moduleName)) true } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt index 9723fdaf..5c5c8ac8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -25,11 +25,13 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.util.FuzzierUtil interface IterationFileCollector { fun collectFiles( targets: List>, shouldContinue: () -> Boolean, + fileFilter: (VirtualFile) -> Boolean, ): List } \ No newline at end of file From 110ef07907f7a16d8d163ac7afa2166e50b66ad0 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:07:47 +0200 Subject: [PATCH 22/61] WIP: Refactor iteration file to not use virtual files --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 13 ++++----- .../mituuz/fuzzier/entities/IterationFIle.kt | 28 +++++++++++++++++++ .../fuzzier/entities/StringEvaluator.kt | 19 ++++++------- .../IntelliJIterationFileCollector.kt | 11 ++++++-- .../iteration/IterationFileCollector.kt | 4 +-- .../com/mituuz/fuzzier/util/FuzzierUtil.kt | 3 -- 6 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 5a33c24f..0c1a76de 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -42,6 +42,7 @@ import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.IterationFile import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector @@ -200,7 +201,7 @@ open class Fuzzier : FuzzyAction() { ) } - private suspend fun collectIterationFiles(project: Project): List { + private suspend fun collectIterationFiles(project: Project): List { val ctx = currentCoroutineContext() val job = ctx.job @@ -226,7 +227,7 @@ open class Fuzzier : FuzzyAction() { * @return a priority list which has been size limited and sorted */ private suspend fun processFiles( - iterationFiles: List, + iterationFiles: List, stringEvaluator: StringEvaluator, searchString: String ): DefaultListModel { @@ -245,7 +246,7 @@ open class Fuzzier : FuzzyAction() { val parallelism = (cores - 1).coerceIn(1, 8) coroutineScope { - val ch = Channel(capacity = parallelism * 2) + val ch = Channel(capacity = parallelism * 2) repeat(parallelism) { launch { @@ -261,7 +262,7 @@ open class Fuzzier : FuzzyAction() { } for (iterationFile in iterationFiles) { - if (!processedFiles.add(iterationFile.file.path)) continue + if (!processedFiles.add(iterationFile.path)) continue ch.send(iterationFile) } ch.close() @@ -380,8 +381,4 @@ open class Fuzzier : FuzzyAction() { 75, ) } - - fun setCollector(iterationFileCollector: IterationFileCollector) { - this.collector = iterationFileCollector - } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt new file mode 100644 index 00000000..c2d7a9a2 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.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.entities + +// I think we can remove the dir check from here +data class IterationFile(val name: String, val path: String, val module: String, val isDirectory: Boolean) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index e6a2e7c4..72a5619c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -25,14 +25,12 @@ package com.mituuz.fuzzier.entities import com.intellij.openapi.roots.ContentIterator import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.util.FuzzierUtil import java.util.concurrent.Future import javax.swing.DefaultListModel /** * 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, @@ -53,7 +51,8 @@ class StringEvaluator( val moduleBasePath = modules[moduleName] ?: return@ContentIterator true val filePath = file.path.removePrefix(moduleBasePath) - if (isExcluded(file, filePath)) { + // TODO: Might have broken VCS check + if (isExcluded(filePath)) { return@ContentIterator true } if (filePath.isNotBlank()) { @@ -79,7 +78,8 @@ class StringEvaluator( if (file.isDirectory) { val moduleBasePath = modules[moduleName] ?: return@ContentIterator true val filePath = getDirPath(file, moduleBasePath, moduleName) - if (isExcluded(file, filePath)) { + // TODO: Might have broken VCS check + if (isExcluded(filePath)) { return@ContentIterator true } if (filePath.isNotBlank()) { @@ -93,16 +93,15 @@ class StringEvaluator( } } - fun evaluateFile(iterationFile: FuzzierUtil.IterationFile, searchString: String): FuzzyMatchContainer? { + fun evaluateFile(iterationFile: IterationFile, searchString: String): FuzzyMatchContainer? { val scoreCalculator = ScoreCalculator(searchString) - val file = iterationFile.file val moduleName = iterationFile.module - if (!file.isDirectory) { + if (!iterationFile.isDirectory) { val moduleBasePath = modules[moduleName] ?: return null - val filePath = file.path.removePrefix(moduleBasePath) - if (isExcluded(file, filePath)) { + val filePath = iterationFile.path.removePrefix(moduleBasePath) + if (isExcluded(filePath)) { return null } if (filePath.isNotBlank()) { @@ -138,7 +137,7 @@ class StringEvaluator( * * @return true if file should be excluded */ - private fun isExcluded(file: VirtualFile, filePath: String): Boolean { + private fun isExcluded(filePath: String): Boolean { return exclusionList.any { e -> when { e.startsWith("*") -> filePath.endsWith(e.substring(1)) diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt index 9a4787d3..d0054e01 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -26,18 +26,23 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.util.FuzzierUtil +import com.mituuz.fuzzier.entities.IterationFile class IntelliJIterationFileCollector : IterationFileCollector { override fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean - ): List = buildList { + ): List = buildList { for ((fileIndex, moduleName) in targets) { fileIndex.iterateContent { vf -> if (!shouldContinue()) return@iterateContent false - if (fileFilter(vf)) add(FuzzierUtil.IterationFile(vf, moduleName)) + + if (fileFilter(vf)) { + val iterationFile = IterationFile(vf.name, vf.path, moduleName, vf.isDirectory) + add(iterationFile) + } + true } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt index 5c5c8ac8..be23cc4e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -26,12 +26,12 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.util.FuzzierUtil +import com.mituuz.fuzzier.entities.IterationFile interface IterationFileCollector { fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean, - ): List + ): List } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index efb1229e..81de8382 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -28,7 +28,6 @@ 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.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService @@ -42,8 +41,6 @@ class FuzzierUtil { 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 From f8830f8468f8054e5d7f632fe9c37b872c0f77eb Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:17:52 +0200 Subject: [PATCH 23/61] Add initial file entries and fix collector test --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 10 +++--- .../{IterationFIle.kt => IterationEntry.kt} | 20 +++++++++-- .../fuzzier/entities/StringEvaluator.kt | 8 ++--- .../IntelliJIterationFileCollector.kt | 8 ++--- .../iteration/IterationFileCollector.kt | 4 +-- .../kotlin/com/mituuz/fuzzier/TestUtil.kt | 2 +- ...t.kt => IntelliJFileEntryCollectorTest.kt} | 33 +++++++++++-------- 7 files changed, 54 insertions(+), 31 deletions(-) rename src/main/kotlin/com/mituuz/fuzzier/entities/{IterationFIle.kt => IterationEntry.kt} (78%) rename src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/{IntelliJIterationFileCollectorTest.kt => IntelliJFileEntryCollectorTest.kt} (84%) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 0c1a76de..96d18459 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -42,7 +42,7 @@ import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.IterationFile +import com.mituuz.fuzzier.entities.FileEntry import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector @@ -201,7 +201,7 @@ open class Fuzzier : FuzzyAction() { ) } - private suspend fun collectIterationFiles(project: Project): List { + private suspend fun collectIterationFiles(project: Project): List { val ctx = currentCoroutineContext() val job = ctx.job @@ -227,7 +227,7 @@ open class Fuzzier : FuzzyAction() { * @return a priority list which has been size limited and sorted */ private suspend fun processFiles( - iterationFiles: List, + fileEntries: List, stringEvaluator: StringEvaluator, searchString: String ): DefaultListModel { @@ -246,7 +246,7 @@ open class Fuzzier : FuzzyAction() { val parallelism = (cores - 1).coerceIn(1, 8) coroutineScope { - val ch = Channel(capacity = parallelism * 2) + val ch = Channel(capacity = parallelism * 2) repeat(parallelism) { launch { @@ -261,7 +261,7 @@ open class Fuzzier : FuzzyAction() { } } - for (iterationFile in iterationFiles) { + for (iterationFile in fileEntries) { if (!processedFiles.add(iterationFile.path)) continue ch.send(iterationFile) } diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt similarity index 78% rename from src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt rename to src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt index c2d7a9a2..43e56353 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationFIle.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt @@ -24,5 +24,21 @@ package com.mituuz.fuzzier.entities -// I think we can remove the dir check from here -data class IterationFile(val name: String, val path: String, val module: String, val isDirectory: Boolean) +sealed interface IterationEntry { + val name: String + val path: String + val module: String +} + +data class FileEntry( + val name: String, + val path: String, + val module: String, + val isDirectory: Boolean = false +) + +data class DirEntry( + val name: String, + val path: String, + val module: String, +) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 72a5619c..99165680 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -93,14 +93,14 @@ class StringEvaluator( } } - fun evaluateFile(iterationFile: IterationFile, searchString: String): FuzzyMatchContainer? { + fun evaluateFile(fileEntry: FileEntry, searchString: String): FuzzyMatchContainer? { val scoreCalculator = ScoreCalculator(searchString) - val moduleName = iterationFile.module + val moduleName = fileEntry.module - if (!iterationFile.isDirectory) { + if (!fileEntry.isDirectory) { val moduleBasePath = modules[moduleName] ?: return null - val filePath = iterationFile.path.removePrefix(moduleBasePath) + val filePath = fileEntry.path.removePrefix(moduleBasePath) if (isExcluded(filePath)) { return null } diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt index d0054e01..2b13a071 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -26,21 +26,21 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.IterationFile +import com.mituuz.fuzzier.entities.FileEntry class IntelliJIterationFileCollector : IterationFileCollector { override fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean - ): List = buildList { + ): List = buildList { for ((fileIndex, moduleName) in targets) { fileIndex.iterateContent { vf -> if (!shouldContinue()) return@iterateContent false if (fileFilter(vf)) { - val iterationFile = IterationFile(vf.name, vf.path, moduleName, vf.isDirectory) - add(iterationFile) + val fileEntry = FileEntry(vf.name, vf.path, moduleName, vf.isDirectory) + add(fileEntry) } true diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt index be23cc4e..9fdf3614 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -26,12 +26,12 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.IterationFile +import com.mituuz.fuzzier.entities.FileEntry interface IterationFileCollector { fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean, - ): List + ): List } \ 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..fdb4d026 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt @@ -93,7 +93,7 @@ class TestUtil { val tempDirPath = myFixture.tempDirPath ignoredFiles.any { ("$tempDirPath/$it") == file.path } } - stringEvaluator = StringEvaluator(exclusionList, map, changeListManager) + stringEvaluator = StringEvaluator(exclusionList, map) } else { stringEvaluator = StringEvaluator(exclusionList, map) } diff --git a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt similarity index 84% rename from src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt rename to src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt index 44ff4d12..745fce58 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollectorTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt @@ -36,7 +36,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.util.concurrent.atomic.AtomicInteger -class IntelliJIterationFileCollectorTest { +class IntelliJFileEntryCollectorTest { private lateinit var collector: IntelliJIterationFileCollector @BeforeEach @@ -46,7 +46,11 @@ class IntelliJIterationFileCollectorTest { @Test fun `collectFiles returns empty list when targets are empty`() { - val result = collector.collectFiles(emptyList()) { true } + val result = collector.collectFiles( + targets = emptyList(), + shouldContinue = { true }, + fileFilter = { true }, + ) assertTrue(result.isEmpty()) } @@ -66,15 +70,16 @@ class IntelliJIterationFileCollectorTest { val calls = AtomicInteger(0) val res = collector.collectFiles( targets = listOf(index to "mod"), - shouldContinue = { calls.incrementAndGet() == 1 } // true for first file, false after + shouldContinue = { calls.incrementAndGet() == 1 }, + fileFilter = { true } ) assertEquals(1, res.size) - assertEquals("a.txt", res[0].file.name) + assertEquals("a.txt", res[0].name) } @Test - fun `skips directories`() { + fun `skips files that match filter`() { val file1 = LightVirtualFile("a.txt") val dir = mockk() val file2 = LightVirtualFile("b.txt") @@ -91,12 +96,13 @@ class IntelliJIterationFileCollectorTest { val res = collector.collectFiles( targets = listOf(index to "mod"), - shouldContinue = { true } + shouldContinue = { true }, + fileFilter = { vf -> !vf.isDirectory } ) assertEquals(2, res.size) - assertEquals("a.txt", res[0].file.name) - assertEquals("b.txt", res[1].file.name) + assertEquals("a.txt", res[0].name) + assertEquals("b.txt", res[1].name) } @Test @@ -125,17 +131,18 @@ class IntelliJIterationFileCollectorTest { val res = collector.collectFiles( targets = listOf(index1 to "mod1", index2 to "mod2"), - shouldContinue = { true } + shouldContinue = { true }, + fileFilter = { true } ) assertEquals(4, res.size) - assertEquals("a.txt", res[0].file.name) + assertEquals("a.txt", res[0].name) assertEquals("mod1", res[0].module) - assertEquals("b.txt", res[1].file.name) + assertEquals("b.txt", res[1].name) assertEquals("mod1", res[1].module) - assertEquals("c.txt", res[2].file.name) + assertEquals("c.txt", res[2].name) assertEquals("mod2", res[2].module) - assertEquals("d.txt", res[3].file.name) + assertEquals("d.txt", res[3].name) assertEquals("mod2", res[3].module) } } \ No newline at end of file From 2b44de270dd0fffca32cafc2953ebcc950ec58bd Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:31:56 +0200 Subject: [PATCH 24/61] Remove change list manager tests --- .../com/mituuz/fuzzier/ExcludeIgnoreTest.kt | 85 +++++-------------- .../kotlin/com/mituuz/fuzzier/TestUtil.kt | 19 +---- 2 files changed, 23 insertions(+), 81 deletions(-) diff --git a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt index 2bbf2242..e879ee5e 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt @@ -1,34 +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 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() @@ -77,50 +73,13 @@ class ExcludeIgnoreTest { 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()) + val filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf()) 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/TestUtil.kt b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt index fdb4d026..05a93caa 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 @@ -39,8 +37,6 @@ 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 +65,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,18 +80,7 @@ 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) - } else { - stringEvaluator = StringEvaluator(exclusionList, map) - } - + val stringEvaluator = StringEvaluator(exclusionList, map) val contentIterator = stringEvaluator.getContentIterator(myFixture.module.name, "", filePathContainer, null) val index = myFixture.module.rootManager.fileIndex runInEdtAndWait { From 63fa78c9c292d4384dbae29eb6def84044f71427 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:33:43 +0200 Subject: [PATCH 25/61] Remove TODO checks. Change list manager was not used from anywhere else --- src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 99165680..0402476d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -51,7 +51,6 @@ class StringEvaluator( val moduleBasePath = modules[moduleName] ?: return@ContentIterator true val filePath = file.path.removePrefix(moduleBasePath) - // TODO: Might have broken VCS check if (isExcluded(filePath)) { return@ContentIterator true } @@ -78,7 +77,6 @@ class StringEvaluator( if (file.isDirectory) { val moduleBasePath = modules[moduleName] ?: return@ContentIterator true val filePath = getDirPath(file, moduleBasePath, moduleName) - // TODO: Might have broken VCS check if (isExcluded(filePath)) { return@ContentIterator true } From 67a1562447e6230f600b761402afc5ac251aa783 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:34:31 +0200 Subject: [PATCH 26/61] Improve docs --- .../kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 0402476d..a06aabe5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -126,14 +126,9 @@ class StringEvaluator( /** * 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(filePath: String): Boolean { return exclusionList.any { e -> From 2817bb18b81d0d5e66d48fd26617c93b37199fc4 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 16:35:50 +0200 Subject: [PATCH 27/61] Delete unnecessary test file --- asd.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 asd.sql diff --git a/asd.sql b/asd.sql deleted file mode 100644 index e69de29b..00000000 From 5332464b8e76e394e6af9f5e706ffa1430d63948 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 17:14:02 +0200 Subject: [PATCH 28/61] Create a new action type and unify actions --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 51 +++---------- .../kotlin/com/mituuz/fuzzier/FuzzyAction.kt | 4 ++ .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 2 - .../kotlin/com/mituuz/fuzzier/FuzzyMover.kt | 22 +++--- .../mituuz/fuzzier/entities/IterationEntry.kt | 16 ++--- .../mituuz/fuzzier/fileaction/FileAction.kt | 72 +++++++++++++++++++ 6 files changed, 109 insertions(+), 58 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 96d18459..cb9f8c25 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -29,10 +29,7 @@ import com.intellij.openapi.application.EDT 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.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 @@ -40,12 +37,8 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent -import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.FileEntry -import com.mituuz.fuzzier.entities.StringEvaluator -import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector -import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector +import com.mituuz.fuzzier.entities.* +import com.mituuz.fuzzier.fileaction.FileAction import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.* import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler @@ -63,14 +56,14 @@ import javax.swing.DefaultListModel import javax.swing.JComponent import javax.swing.KeyStroke -open class Fuzzier : FuzzyAction() { +open class Fuzzier : FileAction() { override var popupTitle = "Fuzzy Search" override var dimensionKey = "FuzzySearchPopup" - private var currentUpdateListContentJob: Job? = null - private var actionScope: CoroutineScope? = null private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null - private var collector: IterationFileCollector = IntelliJIterationFileCollector() + + override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = + { vf -> !vf.isDirectory } override fun runAction(project: Project, actionEvent: AnActionEvent) { setCustomHandlers() @@ -155,7 +148,6 @@ open class Fuzzier : FuzzyAction() { currentUpdateListContentJob?.cancel() currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { - // Create a reference to the current task to check if it has been cancelled component.fileList.setPaintBusy(true) try { @@ -201,33 +193,12 @@ open class Fuzzier : FuzzyAction() { ) } - private 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) - ) - } - - protected open fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = - { vf -> !vf.isDirectory } - /** * Processes a set of IterationFiles concurrently * @return a priority list which has been size limited and sorted */ private suspend fun processFiles( - fileEntries: List, + fileEntries: List, stringEvaluator: StringEvaluator, searchString: String ): DefaultListModel { @@ -261,10 +232,10 @@ open class Fuzzier : FuzzyAction() { } } - for (iterationFile in fileEntries) { - if (!processedFiles.add(iterationFile.path)) continue - ch.send(iterationFile) - } + fileEntries + .filterIsInstance() + .filter { processedFiles.add(it.path) } + .forEach { ch.send(it) } ch.close() } diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt index 7660c1b2..eaff4c0d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt @@ -49,6 +49,8 @@ import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import java.awt.Component import java.awt.Font import java.awt.event.ActionEvent @@ -75,6 +77,8 @@ abstract class FuzzyAction : AnAction() { @Volatile var currentTask: Future<*>? = null val fuzzierUtil = FuzzierUtil() + protected open var currentUpdateListContentJob: Job? = null + protected open var actionScope: CoroutineScope? = null override fun actionPerformed(actionEvent: AnActionEvent) { val project = actionEvent.project diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 33ee1457..40a6f5b4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -77,8 +77,6 @@ open class FuzzyGrep() : FuzzyAction() { 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? = null override fun runAction( project: Project, diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index 7332ed2d..3de54ced 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -45,6 +45,7 @@ import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.StringEvaluator +import com.mituuz.fuzzier.fileaction.FileAction import com.mituuz.fuzzier.util.FuzzierUtil import org.apache.commons.lang3.StringUtils import java.awt.event.ActionEvent @@ -59,12 +60,19 @@ import javax.swing.JComponent import javax.swing.KeyStroke import kotlin.coroutines.cancellation.CancellationException -class FuzzyMover : FuzzyAction() { +class FuzzyMover : FileAction() { override var popupTitle = "Fuzzy File Mover" override var dimensionKey = "FuzzyMoverPopup" lateinit var movableFile: PsiFile lateinit var currentFile: VirtualFile + override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { + if (component.isDirSelector) { + return { vf -> vf.isDirectory } + } + return { vf -> !vf.isDirectory } + } + override fun runAction(project: Project, actionEvent: AnActionEvent) { setCustomHandlers() @@ -183,10 +191,11 @@ class FuzzyMover : FuzzyAction() { currentTask?.takeIf { !it.isDone }?.cancel(true) currentTask = ApplicationManager.getApplication().executeOnPooledThread { + component.fileList.setPaintBusy(true) + 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() @@ -202,17 +211,14 @@ class FuzzyMover : FuzzyAction() { 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) - } + component.refreshModel(listModel, getCellRenderer()) } } catch (_: InterruptedException) { return@executeOnPooledThread } catch (_: CancellationException) { return@executeOnPooledThread + } finally { + component.fileList.setPaintBusy(false) } } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt index 43e56353..0a6fb2a2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt @@ -31,14 +31,14 @@ sealed interface IterationEntry { } data class FileEntry( - val name: String, - val path: String, - val module: String, + override val name: String, + override val path: String, + override val module: String, val isDirectory: Boolean = false -) +) : IterationEntry data class DirEntry( - val name: String, - val path: String, - val module: String, -) + override val name: String, + override val path: String, + override val module: String, +) : IterationEntry diff --git a/src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt b/src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt new file mode 100644 index 00000000..5f991091 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt @@ -0,0 +1,72 @@ +/* + * 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.fileaction + +import com.intellij.openapi.actionSystem.AnActionEvent +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.vfs.VirtualFile +import com.mituuz.fuzzier.FuzzyAction +import com.mituuz.fuzzier.entities.IterationEntry +import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector +import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.job + +abstract class FileAction : FuzzyAction() { + private var collector: IterationFileCollector = IntelliJIterationFileCollector() + + abstract override fun runAction( + project: Project, + actionEvent: AnActionEvent + ) + + abstract override fun createPopup(screenDimensionKey: String): JBPopup + + abstract override fun updateListContents(project: Project, searchString: String) + + abstract fun buildFileFilter(project: Project): (VirtualFile) -> Boolean + + 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) + ) + } +} \ No newline at end of file From c4e6d473e718a6332eb815f099bf2463f4863bb1 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 17:20:14 +0200 Subject: [PATCH 29/61] Use clearer packaging --- .../fuzzier/FuzzyGrepCaseInsensitive.kt | 51 ----- .../fuzzier/{ => actions}/FuzzyAction.kt | 5 +- .../filesystem/FilesystemAction.kt} | 6 +- .../{ => actions/filesystem}/Fuzzier.kt | 25 ++- .../{ => actions/filesystem}/FuzzierVCS.kt | 5 +- .../{ => actions/filesystem}/FuzzyMover.kt | 10 +- .../fuzzier/{ => actions/grep}/FuzzyGrep.kt | 7 +- .../actions/grep/FuzzyGrepCaseInsensitive.kt | 53 +++++ src/main/resources/META-INF/plugin.xml | 188 ++++++++++-------- .../kotlin/com/mituuz/fuzzier/FuzzierTest.kt | 1 + .../com/mituuz/fuzzier/FuzzyActionTest.kt | 1 + .../com/mituuz/fuzzier/FuzzyMoverTest.kt | 69 ++++--- 12 files changed, 230 insertions(+), 191 deletions(-) delete mode 100644 src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt rename src/main/kotlin/com/mituuz/fuzzier/{ => actions}/FuzzyAction.kt (98%) rename src/main/kotlin/com/mituuz/fuzzier/{fileaction/FileAction.kt => actions/filesystem/FilesystemAction.kt} (95%) rename src/main/kotlin/com/mituuz/fuzzier/{ => actions/filesystem}/Fuzzier.kt (93%) rename src/main/kotlin/com/mituuz/fuzzier/{ => actions/filesystem}/FuzzierVCS.kt (97%) rename src/main/kotlin/com/mituuz/fuzzier/{ => actions/filesystem}/FuzzyMover.kt (97%) rename src/main/kotlin/com/mituuz/fuzzier/{ => actions/grep}/FuzzyGrep.kt (98%) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt deleted file mode 100644 index cea184b7..00000000 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt +++ /dev/null @@ -1,51 +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 com.mituuz.fuzzier.entities.FuzzyContainer -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) { - val modifiedCommands = commands.toMutableList() - if (isWindows && !useRg) { - // Customize findstr for case insensitivity - modifiedCommands.add(1, "/I") - } else if (!useRg) { - // Customize grep for case insensitivity - modifiedCommands.add(1, "-i") - } else { - // Customize ripgrep for case insensitivity - modifiedCommands.add(1, "--smart-case") - modifiedCommands.add(2, "-F") - } - super.runCommand(modifiedCommands, listModel, projectBasePath) - } -} diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index eaff4c0d..7904f2b6 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.* @@ -43,6 +43,7 @@ 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 @@ -172,7 +173,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 ) diff --git a/src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt similarity index 95% rename from src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index 5f991091..2384de87 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/fileaction/FileAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.fileaction +package com.mituuz.fuzzier.actions.filesystem import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.module.ModuleManager @@ -31,14 +31,14 @@ import com.intellij.openapi.project.rootManager import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.FuzzyAction +import com.mituuz.fuzzier.actions.FuzzyAction import com.mituuz.fuzzier.entities.IterationEntry import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.job -abstract class FileAction : FuzzyAction() { +abstract class FilesystemAction : FuzzyAction() { private var collector: IterationFileCollector = IntelliJIterationFileCollector() abstract override fun runAction( diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt similarity index 93% rename from src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index cb9f8c25..508c3714 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -21,7 +21,8 @@ * 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.filesystem import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager @@ -38,8 +39,7 @@ import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.* -import com.mituuz.fuzzier.fileaction.FileAction -import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.RecentFilesMode.* +import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler import kotlinx.coroutines.* @@ -56,7 +56,7 @@ import javax.swing.DefaultListModel import javax.swing.JComponent import javax.swing.KeyStroke -open class Fuzzier : FileAction() { +open class Fuzzier : FilesystemAction() { override var popupTitle = "Fuzzy Search" override var dimensionKey = "FuzzySearchPopup" private var previewAlarm: SingleAlarm? = null @@ -82,7 +82,7 @@ open class Fuzzier : FileAction() { (component as FuzzyFinderComponent).splitPane.dividerLocation = globalState.splitPosition - if (globalState.recentFilesMode != NONE) { + if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) { createInitialView(project) } } @@ -115,17 +115,20 @@ open class Fuzzier : FileAction() { component.fileList.setPaintBusy(true) ApplicationManager.getApplication().executeOnPooledThread { try { - val editorHistoryManager = EditorHistoryManager.getInstance(project) + val editorHistoryManager = EditorHistoryManager.Companion.getInstance(project) val listModel = when (globalState.recentFilesMode) { - RECENT_PROJECT_FILES -> InitialViewHandler.getRecentProjectFiles( + FuzzierGlobalSettingsService.RecentFilesMode.RECENT_PROJECT_FILES -> InitialViewHandler.Companion.getRecentProjectFiles( globalState, fuzzierUtil, editorHistoryManager, project ) - RECENTLY_SEARCHED_FILES -> InitialViewHandler.getRecentlySearchedFiles(projectState) + FuzzierGlobalSettingsService.RecentFilesMode.RECENTLY_SEARCHED_FILES -> InitialViewHandler.Companion.getRecentlySearchedFiles( + projectState + ) + else -> { DefaultListModel() } @@ -172,7 +175,7 @@ open class Fuzzier : FileAction() { } private fun handleEmptySearchString(project: Project) { - if (globalState.recentFilesMode != NONE) { + if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) { createInitialView(project) } else { ApplicationManager.getApplication().invokeLater { @@ -202,7 +205,7 @@ open class Fuzzier : FileAction() { stringEvaluator: StringEvaluator, searchString: String ): DefaultListModel { - val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) + val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) val processedFiles = ConcurrentHashMap.newKeySet() val listLimit = globalState.fileListLimit val priorityQueue = PriorityQueue( @@ -283,7 +286,7 @@ open class Fuzzier : FileAction() { } } if (fuzzyContainer != null) { - InitialViewHandler.addFileToRecentlySearchedFiles(fuzzyContainer, projectState, globalState) + InitialViewHandler.Companion.addFileToRecentlySearchedFiles(fuzzyContainer, projectState, globalState) } popup.cancel() } diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt similarity index 97% rename from src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt index 6720f6f8..0a0a17bb 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt @@ -21,7 +21,8 @@ * 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.filesystem import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.changes.ChangeListManager @@ -35,6 +36,6 @@ class FuzzierVCS : Fuzzier() { override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { val clm = ChangeListManager.getInstance(project) - return { vf -> !vf.isDirectory && !clm.isIgnoredFile(vf)} + return { vf -> !vf.isDirectory && !clm.isIgnoredFile(vf) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt similarity index 97% rename from src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index 3de54ced..a2c3e05f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -21,7 +21,8 @@ * 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.filesystem import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -45,7 +46,6 @@ import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.StringEvaluator -import com.mituuz.fuzzier.fileaction.FileAction import com.mituuz.fuzzier.util.FuzzierUtil import org.apache.commons.lang3.StringUtils import java.awt.event.ActionEvent @@ -60,7 +60,7 @@ import javax.swing.JComponent import javax.swing.KeyStroke import kotlin.coroutines.cancellation.CancellationException -class FuzzyMover : FileAction() { +class FuzzyMover : FilesystemAction() { override var popupTitle = "Fuzzy File Mover" override var dimensionKey = "FuzzyMoverPopup" lateinit var movableFile: PsiFile @@ -238,8 +238,8 @@ class FuzzyMover : FileAction() { project: Project, stringEvaluator: StringEvaluator, searchString: String, listModel: DefaultListModel, task: Future<*>? ) { - val moduleManager = ModuleManager.getInstance(project) - val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) + val moduleManager = ModuleManager.Companion.getInstance(project) + val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) if (projectState.isProject) { processProject(project, stringEvaluator, ss, listModel, task) } else { diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt rename to src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt index 40a6f5b4..288a61d6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier +package com.mituuz.fuzzier.actions.grep import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.OSProcessHandler @@ -45,8 +45,9 @@ 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.actions.grep.FuzzyGrep.Companion.MAX_NUMBER_OR_RESULTS +import com.mituuz.fuzzier.actions.grep.FuzzyGrep.Companion.MAX_OUTPUT_SIZE import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.RowContainer diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt new file mode 100644 index 00000000..d1d288d1 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt @@ -0,0 +1,53 @@ +/* + * 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.grep + +import com.mituuz.fuzzier.entities.FuzzyContainer +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 + ) { + val modifiedCommands = commands.toMutableList() + if (isWindows && !useRg) { + // Customize findstr for case insensitivity + modifiedCommands.add(1, "/I") + } else if (!useRg) { + // Customize grep for case insensitivity + modifiedCommands.add(1, "-i") + } else { + // Customize ripgrep for case insensitivity + modifiedCommands.add(1, "--smart-case") + modifiedCommands.add(2, "-F") + } + super.runCommand(modifiedCommands, listModel, projectBasePath) + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 823d13d1..fb1af69c 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/FuzzierTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt index 35939958..64fe4e24 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt @@ -24,6 +24,7 @@ package com.mituuz.fuzzier import com.intellij.testFramework.TestApplicationManager +import com.mituuz.fuzzier.actions.filesystem.Fuzzier import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertFalse diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt index cbfe5cc5..b1a5ab09 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt @@ -32,6 +32,7 @@ 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 diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt index f5927a28..e55e02d6 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.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 @@ -35,6 +33,7 @@ import com.intellij.psi.PsiManager import com.intellij.testFramework.LightVirtualFile import com.intellij.testFramework.TestApplicationManager import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.mituuz.fuzzier.actions.filesystem.FuzzyMover import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer @@ -42,7 +41,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,12 +62,13 @@ 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{ + fuzzyMover.handleInput(project).thenRun { var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope") assertNotNull(targetFile) targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope") @@ -89,7 +88,8 @@ 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 @@ -98,7 +98,7 @@ class FuzzyMoverTest { fuzzyMover.component.fileList.model = getListModel(virtualDir) fuzzyMover.component.fileList.selectedIndex = 0 - fuzzyMover.handleInput(project).thenRun{ + fuzzyMover.handleInput(project).thenRun { var targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/asd/nope") assertNotNull(targetFile) targetFile = VirtualFileManager.getInstance().findFileByUrl("file://$basePath/nope") @@ -123,7 +123,8 @@ 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 @@ -132,7 +133,7 @@ class FuzzyMoverTest { fuzzyMover.component.fileList.model = getListModel(virtualDir) fuzzyMover.component.fileList.selectedIndex = 0 - fuzzyMover.handleInput(project).thenRun{ + fuzzyMover.handleInput(project).thenRun { var targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/test/main.kt") assertNotNull(targetFile) targetFile = VirtualFileManager.getInstance().findFileByUrl("$basePath/main.kt") @@ -165,9 +166,10 @@ class FuzzyMoverTest { fuzzyMover.handleInput(project).join() fuzzyMover.component.fileList.model = getListModel(virtualDir) fuzzyMover.component.fileList.selectedIndex = 0 - fuzzyMover.popup = JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup() + fuzzyMover.popup = + JBPopupFactory.getInstance().createComponentPopupBuilder(fuzzyMover.component, null).createPopup() - fuzzyMover.handleInput(project).thenRun{ + fuzzyMover.handleInput(project).thenRun { var targetFile = VirtualFileManager.getInstance().findFileByUrl("$module2BasePath/target/MoveMe.kt") assertNotNull(targetFile) targetFile = VirtualFileManager.getInstance().findFileByUrl("$module1BasePath/MoveMe.kt") @@ -180,7 +182,8 @@ class FuzzyMoverTest { 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, "") listModel.addElement(container) } return listModel From 00743de30978812a3ef91466cc158020a0a7424f Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 17:30:47 +0200 Subject: [PATCH 30/61] Use generic handler because we dont need more --- .../fuzzier/actions/filesystem/Fuzzier.kt | 10 ++++++---- .../fuzzier/entities/StringEvaluator.kt | 20 ++++++++----------- .../IntelliJIterationFileCollector.kt | 8 ++++---- .../iteration/IterationFileCollector.kt | 4 ++-- ... => IntelliJIteratorEntryCollectorTest.kt} | 2 +- 5 files changed, 21 insertions(+), 23 deletions(-) rename src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/{IntelliJFileEntryCollectorTest.kt => IntelliJIteratorEntryCollectorTest.kt} (99%) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index 508c3714..d29f454f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -38,7 +38,10 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent -import com.mituuz.fuzzier.entities.* +import com.mituuz.fuzzier.entities.FuzzyContainer +import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.IterationEntry +import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler @@ -220,12 +223,12 @@ open class Fuzzier : FilesystemAction() { val parallelism = (cores - 1).coerceIn(1, 8) coroutineScope { - val ch = Channel(capacity = parallelism * 2) + val ch = Channel(capacity = parallelism * 2) repeat(parallelism) { launch { for (iterationFile in ch) { - val container = stringEvaluator.evaluateFile(iterationFile, ss) + val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) container?.let { fuzzyMatchContainer -> synchronized(queueLock) { minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) @@ -236,7 +239,6 @@ open class Fuzzier : FilesystemAction() { } fileEntries - .filterIsInstance() .filter { processedFiles.add(it.path) } .forEach { ch.send(it) } ch.close() diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index a06aabe5..4289fc49 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -91,21 +91,17 @@ class StringEvaluator( } } - fun evaluateFile(fileEntry: FileEntry, searchString: String): FuzzyMatchContainer? { + fun evaluateIteratorEntry(iteratorEntry: IterationEntry, searchString: String): FuzzyMatchContainer? { val scoreCalculator = ScoreCalculator(searchString) - val moduleName = fileEntry.module + val moduleName = iteratorEntry.module - if (!fileEntry.isDirectory) { - val moduleBasePath = modules[moduleName] ?: return null + val moduleBasePath = modules[moduleName] ?: return null - val filePath = fileEntry.path.removePrefix(moduleBasePath) - if (isExcluded(filePath)) { - return null - } - if (filePath.isNotBlank()) { - val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator) - return fuzzyMatchContainer - } + val dirPath = iteratorEntry.path.removePrefix(moduleBasePath) + if (isExcluded(dirPath)) return null + + if (dirPath.isNotBlank()) { + return createFuzzyContainer(dirPath, moduleBasePath, scoreCalculator) } return null diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt index 2b13a071..f6b24d0b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -26,21 +26,21 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.FileEntry +import com.mituuz.fuzzier.entities.IteratorEntry class IntelliJIterationFileCollector : IterationFileCollector { override fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean - ): List = buildList { + ): List = buildList { for ((fileIndex, moduleName) in targets) { fileIndex.iterateContent { vf -> if (!shouldContinue()) return@iterateContent false if (fileFilter(vf)) { - val fileEntry = FileEntry(vf.name, vf.path, moduleName, vf.isDirectory) - add(fileEntry) + val iteratorEntry = IteratorEntry(vf.name, vf.path, moduleName, vf.isDirectory) + add(iteratorEntry) } true diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt index 9fdf3614..05be4478 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -26,12 +26,12 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.FileEntry +import com.mituuz.fuzzier.entities.IteratorEntry interface IterationFileCollector { fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean, - ): List + ): List } \ No newline at end of file diff --git a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt similarity index 99% rename from src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt rename to src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt index 745fce58..de7eca70 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJFileEntryCollectorTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIteratorEntryCollectorTest.kt @@ -36,7 +36,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.util.concurrent.atomic.AtomicInteger -class IntelliJFileEntryCollectorTest { +class IntelliJIteratorEntryCollectorTest { private lateinit var collector: IntelliJIterationFileCollector @BeforeEach From 769010912bfa6d71d636c633f8c3b3e2556a06f5 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 17:48:59 +0200 Subject: [PATCH 31/61] Unify fuzzy mover handling --- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 3 - .../actions/filesystem/FilesystemAction.kt | 13 ++ .../fuzzier/actions/filesystem/Fuzzier.kt | 17 +- .../fuzzier/actions/filesystem/FuzzyMover.kt | 158 ++++++++++-------- .../mituuz/fuzzier/entities/IterationEntry.kt | 31 ++-- .../IntelliJIterationFileCollector.kt | 6 +- .../iteration/IterationFileCollector.kt | 4 +- 7 files changed, 121 insertions(+), 111 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 7904f2b6..1bd61a14 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -58,7 +58,6 @@ 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 @@ -75,8 +74,6 @@ 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 diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index 2384de87..06067655 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -33,6 +33,7 @@ import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.actions.FuzzyAction import com.mituuz.fuzzier.entities.IterationEntry +import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector import kotlinx.coroutines.currentCoroutineContext @@ -69,4 +70,16 @@ abstract class FilesystemAction : FuzzyAction() { fileFilter = buildFileFilter(project) ) } + + fun getStringEvaluator(): StringEvaluator { + val combinedExclusions = buildSet { + addAll(projectState.exclusionSet) + addAll(globalState.globalExclusionSet) + } + return StringEvaluator( + combinedExclusions, + projectState.modules, + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index d29f454f..8d90abf6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -160,13 +160,13 @@ open class Fuzzier : FilesystemAction() { val stringEvaluator = getStringEvaluator() coroutineContext.ensureActive() - val iterationFiles = withContext(Dispatchers.Default) { + val iterationEntries = withContext(Dispatchers.Default) { collectIterationFiles(project) } coroutineContext.ensureActive() val listModel = withContext(Dispatchers.Default) { - processFiles(iterationFiles, stringEvaluator, searchString) + processIterationEntries(iterationEntries, stringEvaluator, searchString) } coroutineContext.ensureActive() @@ -188,22 +188,11 @@ open class Fuzzier : FilesystemAction() { } } - private 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 */ - private suspend fun processFiles( + private suspend fun processIterationEntries( fileEntries: List, stringEvaluator: StringEvaluator, searchString: String diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index a2c3e05f..70687122 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -29,12 +29,10 @@ import com.intellij.notification.NotificationType import com.intellij.notification.Notifications import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT 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 @@ -45,20 +43,24 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer +import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.IterationEntry import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.util.FuzzierUtil +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel 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.* import java.util.concurrent.CompletableFuture -import java.util.concurrent.Future +import java.util.concurrent.ConcurrentHashMap import javax.swing.AbstractAction import javax.swing.DefaultListModel import javax.swing.JComponent import javax.swing.KeyStroke -import kotlin.coroutines.cancellation.CancellationException class FuzzyMover : FilesystemAction() { override var popupTitle = "Fuzzy File Mover" @@ -67,15 +69,15 @@ class FuzzyMover : FilesystemAction() { lateinit var currentFile: VirtualFile override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { - if (component.isDirSelector) { - return { vf -> vf.isDirectory } - } - return { vf -> !vf.isDirectory } + return { vf -> if (component.isDirSelector) vf.isDirectory else !vf.isDirectory } } override fun runAction(project: Project, actionEvent: AnActionEvent) { setCustomHandlers() + actionScope?.cancel() + actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + ApplicationManager.getApplication().invokeLater { component = SimpleFinderComponent() createListeners(project) @@ -96,7 +98,11 @@ class FuzzyMover : FilesystemAction() { popup.addListener(object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { resetOriginalHandlers() - super.onClosed(event) + + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = null + + actionScope?.cancel() } }) @@ -189,89 +195,95 @@ class FuzzyMover : FilesystemAction() { return } - currentTask?.takeIf { !it.isDone }?.cancel(true) - currentTask = ApplicationManager.getApplication().executeOnPooledThread { + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { component.fileList.setPaintBusy(true) try { - // Create a reference to the current task to check if it has been cancelled - val task = currentTask - var listModel = DefaultListModel() - val stringEvaluator = getStringEvaluator() + coroutineContext.ensureActive() - 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 + val iterationEntries = withContext(Dispatchers.Default) { + collectIterationFiles(project) + } + coroutineContext.ensureActive() - ApplicationManager.getApplication().invokeLater { - component.refreshModel(listModel, getCellRenderer()) + val listModel = withContext(Dispatchers.Default) { + processIterationEntries(iterationEntries, stringEvaluator, searchString) } - } catch (_: InterruptedException) { - return@executeOnPooledThread - } catch (_: CancellationException) { - return@executeOnPooledThread + coroutineContext.ensureActive() + + component.refreshModel(listModel, getCellRenderer()) } finally { component.fileList.setPaintBusy(false) } } } - private fun getStringEvaluator(): StringEvaluator { - val combinedExclusions = buildSet { - addAll(projectState.exclusionSet) - addAll(globalState.globalExclusionSet) - } - return StringEvaluator( - combinedExclusions, - projectState.modules + private suspend fun processIterationEntries( + fileEntries: List, + stringEvaluator: StringEvaluator, + searchString: String + ): DefaultListModel { + val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) + val processedFiles = ConcurrentHashMap.newKeySet() + val listLimit = globalState.fileListLimit + val priorityQueue = PriorityQueue( + listLimit + 1, + compareBy { it.getScore() } ) - } - private fun process( - project: Project, stringEvaluator: StringEvaluator, searchString: String, - listModel: DefaultListModel, task: Future<*>? - ) { - val moduleManager = ModuleManager.Companion.getInstance(project) - val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) - if (projectState.isProject) { - processProject(project, stringEvaluator, ss, listModel, task) - } else { - processModules(moduleManager, stringEvaluator, ss, listModel, task) - } - } + val queueLock = Any() + var minimumScore: Int? = null - 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) + 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 container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + } + } + } + } + } + + fileEntries + .filter { processedFiles.add(it.path) } + .forEach { ch.send(it) } + ch.close() } - ProjectFileIndex.getInstance(project).iterateContent(contentIterator) + + + val result = DefaultListModel() + result.addAll( + priorityQueue.sortedWith( + compareByDescending { it.getScore() }) + ) + return result } - 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) + private fun PriorityQueue.maybeAdd( + minimumScore: Int?, + fuzzyMatchContainer: FuzzyMatchContainer + ): Int? { + var ret = minimumScore + + if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + this.add(fuzzyMatchContainer) + if (this.size > globalState.fileListLimit) { + this.remove() + ret = this.peek().getScore() } - moduleFileIndex.iterateContent(contentIterator) } + + return ret } } \ 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 index 0a6fb2a2..58701ce2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt @@ -24,21 +24,20 @@ package com.mituuz.fuzzier.entities -sealed interface IterationEntry { - val name: String - val path: String +data class IterationEntry( + val name: String, + val path: String, val module: String -} +) -data class FileEntry( - override val name: String, - override val path: String, - override val module: String, - val isDirectory: Boolean = false -) : IterationEntry - -data class DirEntry( - override val name: String, - override val path: String, - override val module: String, -) : IterationEntry +//data class FileEntry( +// override val name: String, +// override val path: String, +// override val module: String, +//) : IterationEntry +// +//data class DirEntry( +// override val name: String, +// override val path: String, +// override val module: String, +//) : IterationEntry diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt index f6b24d0b..7b6d69a4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -26,20 +26,20 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.IteratorEntry +import com.mituuz.fuzzier.entities.IterationEntry class IntelliJIterationFileCollector : IterationFileCollector { override fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean - ): List = buildList { + ): List = buildList { for ((fileIndex, moduleName) in targets) { fileIndex.iterateContent { vf -> if (!shouldContinue()) return@iterateContent false if (fileFilter(vf)) { - val iteratorEntry = IteratorEntry(vf.name, vf.path, moduleName, vf.isDirectory) + val iteratorEntry = IterationEntry(vf.name, vf.path, moduleName) add(iteratorEntry) } diff --git a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt index 05be4478..b142c938 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IterationFileCollector.kt @@ -26,12 +26,12 @@ package com.mituuz.fuzzier.intellij.iteration import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.IteratorEntry +import com.mituuz.fuzzier.entities.IterationEntry interface IterationFileCollector { fun collectFiles( targets: List>, shouldContinue: () -> Boolean, fileFilter: (VirtualFile) -> Boolean, - ): List + ): List } \ No newline at end of file From cf13e7d99344470b6b1748d8d9686d9e30115679 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 13 Dec 2025 18:02:04 +0200 Subject: [PATCH 32/61] Finish extracting common code --- .../actions/filesystem/FilesystemAction.kt | 120 +++++++++- .../fuzzier/actions/filesystem/Fuzzier.kt | 222 +++++------------- .../fuzzier/actions/filesystem/FuzzyMover.kt | 121 +--------- .../mituuz/fuzzier/entities/IterationEntry.kt | 12 - .../fuzzier/entities/StringEvaluator.kt | 38 --- 5 files changed, 185 insertions(+), 328 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index 06067655..9ef4ecc9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -25,19 +25,33 @@ 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.ui.popup.JBPopup import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.runtime.loader.IntellijLoader.launch import com.mituuz.fuzzier.actions.FuzzyAction +import com.mituuz.fuzzier.entities.FuzzyContainer +import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.entities.IterationEntry import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector +import com.mituuz.fuzzier.util.FuzzierUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.PriorityQueue +import java.util.concurrent.ConcurrentHashMap +import javax.swing.DefaultListModel abstract class FilesystemAction : FuzzyAction() { private var collector: IterationFileCollector = IntelliJIterationFileCollector() @@ -49,10 +63,10 @@ abstract class FilesystemAction : FuzzyAction() { abstract override fun createPopup(screenDimensionKey: String): JBPopup - abstract override fun updateListContents(project: Project, searchString: String) - 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 @@ -82,4 +96,106 @@ abstract class FilesystemAction : FuzzyAction() { ) } + /** + * 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 + ): DefaultListModel { + val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) + val processedFiles = ConcurrentHashMap.newKeySet() + val listLimit = globalState.fileListLimit + val priorityQueue = PriorityQueue( + listLimit + 1, + compareBy { it.getScore() } + ) + + 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 container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + } + } + } + } + } + + fileEntries + .filter { processedFiles.add(it.path) } + .forEach { ch.send(it) } + ch.close() + } + + + val result = DefaultListModel() + result.addAll( + priorityQueue.sortedWith( + compareByDescending { it.getScore() }) + ) + return result + } + + private fun PriorityQueue.maybeAdd( + minimumScore: Int?, + fuzzyMatchContainer: FuzzyMatchContainer + ): Int? { + var ret = minimumScore + + if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + this.add(fuzzyMatchContainer) + if (this.size > globalState.fileListLimit) { + this.remove() + ret = this.peek().getScore() + } + } + + 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) + } + 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/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index 8d90abf6..f7313a4c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -26,7 +26,6 @@ package com.mituuz.fuzzier.actions.filesystem import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.EDT import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.EditorHistoryManager @@ -39,21 +38,16 @@ import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.IterationEntry -import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService -import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.InitialViewHandler -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import org.apache.commons.lang3.StringUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import java.awt.event.ActionEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.util.* -import java.util.concurrent.ConcurrentHashMap import javax.swing.AbstractAction import javax.swing.DefaultListModel import javax.swing.JComponent @@ -114,6 +108,59 @@ open class Fuzzier : FilesystemAction() { return popup } + override fun handleEmptySearchString(project: Project) { + if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) { + createInitialView(project) + } else { + ApplicationManager.getApplication().invokeLater { + component.fileList.model = DefaultListModel() + defaultDoc?.let { (component as FuzzyFinderComponent).previewPane.updateFile(it) } + } + } + } + + 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() + } + } + + // 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) + } + } + }) + } + private fun createInitialView(project: Project) { component.fileList.setPaintBusy(true) ApplicationManager.getApplication().executeOnPooledThread { @@ -146,119 +193,6 @@ open class Fuzzier : FilesystemAction() { } } - override fun updateListContents(project: Project, searchString: String) { - if (StringUtils.isBlank(searchString)) { - 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) - } - coroutineContext.ensureActive() - - component.refreshModel(listModel, getCellRenderer()) - } finally { - component.fileList.setPaintBusy(false) - } - } - } - - private fun handleEmptySearchString(project: Project) { - if (globalState.recentFilesMode != FuzzierGlobalSettingsService.RecentFilesMode.NONE) { - createInitialView(project) - } else { - ApplicationManager.getApplication().invokeLater { - component.fileList.model = DefaultListModel() - defaultDoc?.let { (component as FuzzyFinderComponent).previewPane.updateFile(it) } - } - } - } - - /** - * Processes a set of IterationFiles concurrently - * @return a priority list which has been size limited and sorted - */ - private suspend fun processIterationEntries( - fileEntries: List, - stringEvaluator: StringEvaluator, - searchString: String - ): DefaultListModel { - val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) - val processedFiles = ConcurrentHashMap.newKeySet() - val listLimit = globalState.fileListLimit - val priorityQueue = PriorityQueue( - listLimit + 1, - compareBy { it.getScore() } - ) - - 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 container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) - container?.let { fuzzyMatchContainer -> - synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) - } - } - } - } - } - - fileEntries - .filter { processedFiles.add(it.path) } - .forEach { ch.send(it) } - ch.close() - } - - - val result = DefaultListModel() - result.addAll( - priorityQueue.sortedWith( - compareByDescending { it.getScore() }) - ) - return result - } - - private fun PriorityQueue.maybeAdd( - minimumScore: Int?, - fuzzyMatchContainer: FuzzyMatchContainer - ): Int? { - var ret = minimumScore - - if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { - this.add(fuzzyMatchContainer) - if (this.size > globalState.fileListLimit) { - this.remove() - ret = this.peek().getScore() - } - } - - return ret - } - private fun openFile(project: Project, fuzzyContainer: FuzzyContainer?, virtualFile: VirtualFile) { val fileEditorManager = FileEditorManager.getInstance(project) val currentEditor = fileEditorManager.selectedTextEditor @@ -282,48 +216,6 @@ open class Fuzzier : FilesystemAction() { 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) { - return@addListSelectionListener - } else { - previewAlarm?.cancelAndRequest() - } - } - - // 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) - } - } - }) - } - private fun getPreviewAlarm(): SingleAlarm { return SingleAlarm( { diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index 70687122..b58a971d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -29,7 +29,6 @@ import com.intellij.notification.NotificationType import com.intellij.notification.Notifications import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.EDT import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project @@ -42,21 +41,15 @@ import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent -import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.IterationEntry -import com.mituuz.fuzzier.entities.StringEvaluator -import com.mituuz.fuzzier.util.FuzzierUtil -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import org.apache.commons.lang3.StringUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import java.awt.event.ActionEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.util.* import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap import javax.swing.AbstractAction import javax.swing.DefaultListModel import javax.swing.JComponent @@ -109,6 +102,12 @@ class FuzzyMover : FilesystemAction() { 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() { @@ -186,104 +185,4 @@ class FuzzyMover : FilesystemAction() { } return completableFuture } - - override fun updateListContents(project: Project, searchString: String) { - if (StringUtils.isBlank(searchString)) { - ApplicationManager.getApplication().invokeLater { - component.fileList.model = DefaultListModel() - } - 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) - } - coroutineContext.ensureActive() - - component.refreshModel(listModel, getCellRenderer()) - } finally { - component.fileList.setPaintBusy(false) - } - } - } - - private suspend fun processIterationEntries( - fileEntries: List, - stringEvaluator: StringEvaluator, - searchString: String - ): DefaultListModel { - val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) - val processedFiles = ConcurrentHashMap.newKeySet() - val listLimit = globalState.fileListLimit - val priorityQueue = PriorityQueue( - listLimit + 1, - compareBy { it.getScore() } - ) - - 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 container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) - container?.let { fuzzyMatchContainer -> - synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) - } - } - } - } - } - - fileEntries - .filter { processedFiles.add(it.path) } - .forEach { ch.send(it) } - ch.close() - } - - - val result = DefaultListModel() - result.addAll( - priorityQueue.sortedWith( - compareByDescending { it.getScore() }) - ) - return result - } - - private fun PriorityQueue.maybeAdd( - minimumScore: Int?, - fuzzyMatchContainer: FuzzyMatchContainer - ): Int? { - var ret = minimumScore - - if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { - this.add(fuzzyMatchContainer) - if (this.size > globalState.fileListLimit) { - this.remove() - ret = this.peek().getScore() - } - } - - return ret - } } \ 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 index 58701ce2..7358482f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt @@ -29,15 +29,3 @@ data class IterationEntry( val path: String, val module: String ) - -//data class FileEntry( -// override val name: String, -// override val path: String, -// override val module: String, -//) : IterationEntry -// -//data class DirEntry( -// override val name: String, -// override val path: String, -// override val module: String, -//) : IterationEntry diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 4289fc49..20558fa2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -65,32 +65,6 @@ class StringEvaluator( } } - 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(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): FuzzyMatchContainer? { val scoreCalculator = ScoreCalculator(searchString) val moduleName = iteratorEntry.module @@ -107,18 +81,6 @@ class StringEvaluator( return null } - 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 - } - /** * Checks if file should be excluded from the results. * From 8921837fee2322d6c031915f54b91b63c972a1bf Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 06:01:59 +0200 Subject: [PATCH 33/61] Use a popup provider instead of inheritance --- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 37 ---------- .../actions/filesystem/FilesystemAction.kt | 14 +--- .../fuzzier/actions/filesystem/Fuzzier.kt | 25 +++++-- .../fuzzier/actions/filesystem/FuzzyMover.kt | 25 +++++-- .../mituuz/fuzzier/actions/grep/FuzzyGrep.kt | 27 +++++--- .../mituuz/fuzzier/ui/DefaultPopupProvider.kt | 69 +++++++++++++++++++ .../com/mituuz/fuzzier/ui/PopupProvider.kt | 47 +++++++++++++ 7 files changed, 173 insertions(+), 71 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt create mode 100644 src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 1bd61a14..3b2399f1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -39,9 +39,6 @@ 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 @@ -49,7 +46,6 @@ import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil -import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import java.awt.Component @@ -89,39 +85,6 @@ abstract class FuzzyAction : AnAction() { 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) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index 9ef4ecc9..e30d5d15 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -30,9 +30,7 @@ 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.vfs.VirtualFile -import com.intellij.platform.runtime.loader.IntellijLoader.launch import com.mituuz.fuzzier.actions.FuzzyAction import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer @@ -41,15 +39,9 @@ import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.intellij.iteration.IntelliJIterationFileCollector import com.mituuz.fuzzier.intellij.iteration.IterationFileCollector import com.mituuz.fuzzier.util.FuzzierUtil -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.PriorityQueue +import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.swing.DefaultListModel @@ -61,8 +53,6 @@ abstract class FilesystemAction : FuzzyAction() { actionEvent: AnActionEvent ) - abstract override fun createPopup(screenDimensionKey: String): JBPopup - abstract fun buildFileFilter(project: Project): (VirtualFile) -> Boolean abstract fun handleEmptySearchString(project: Project) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index f7313a4c..69eefedc 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -30,7 +30,6 @@ import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.EditorHistoryManager 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.vfs.VirtualFile @@ -39,6 +38,8 @@ import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService +import com.mituuz.fuzzier.ui.DefaultPopupProvider +import com.mituuz.fuzzier.ui.PopupConfig import com.mituuz.fuzzier.util.InitialViewHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -58,6 +59,7 @@ open class Fuzzier : FilesystemAction() { override var dimensionKey = "FuzzySearchPopup" private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null + private val popupProvider = DefaultPopupProvider() override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = { vf -> !vf.isDirectory } @@ -73,7 +75,20 @@ open class Fuzzier : FilesystemAction() { component = FuzzyFinderComponent(project) previewAlarm = getPreviewAlarm() createListeners(project) - showPopup(project) + popup = popupProvider.show( + project = project, + content = component, + focus = component.searchField, + config = PopupConfig( + title = popupTitle, + preferredSizeProvider = component.preferredSize, + dimensionKey = dimensionKey, + resetWindow = { globalState.resetWindow }, + clearResetWindowFlag = { globalState.resetWindow = false } + ) + ) + createPopup() + createSharedListeners(project) (component as FuzzyFinderComponent).splitPane.dividerLocation = @@ -85,9 +100,7 @@ open class Fuzzier : FilesystemAction() { } } - override fun createPopup(screenDimensionKey: String): JBPopup { - val popup = getInitialPopup(screenDimensionKey) - + fun createPopup() { popup.addListener(object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { globalState.splitPosition = @@ -104,8 +117,6 @@ open class Fuzzier : FilesystemAction() { lastPreviewKey = null } }) - - return popup } override fun handleEmptySearchString(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index b58a971d..fd381a4b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -32,7 +32,6 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction 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.vfs.VirtualFile @@ -41,6 +40,8 @@ import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent +import com.mituuz.fuzzier.ui.DefaultPopupProvider +import com.mituuz.fuzzier.ui.PopupConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -60,6 +61,7 @@ class FuzzyMover : FilesystemAction() { override var dimensionKey = "FuzzyMoverPopup" lateinit var movableFile: PsiFile lateinit var currentFile: VirtualFile + private val popupProvider = DefaultPopupProvider() override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { return { vf -> if (component.isDirSelector) vf.isDirectory else !vf.isDirectory } @@ -80,14 +82,25 @@ class FuzzyMover : FilesystemAction() { component.fileList.setEmptyText("Press enter to use current file: ${currentFile.path}") } - showPopup(project) + // showPopup(project) + popup = popupProvider.show( + project = project, + content = component, + focus = component.searchField, + config = PopupConfig( + title = popupTitle, + preferredSizeProvider = component.preferredSize, + dimensionKey = dimensionKey, + resetWindow = { globalState.resetWindow }, + clearResetWindowFlag = { globalState.resetWindow = false } + ) + ) + createPopup() createSharedListeners(project) } } - override fun createPopup(screenDimensionKey: String): JBPopup { - val popup = getInitialPopup(screenDimensionKey) - + fun createPopup() { popup.addListener(object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { resetOriginalHandlers() @@ -98,8 +111,6 @@ class FuzzyMover : FilesystemAction() { actionScope?.cancel() } }) - - return popup } override fun handleEmptySearchString(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt index 288a61d6..df2772b1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt @@ -39,7 +39,6 @@ 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 @@ -51,6 +50,8 @@ import com.mituuz.fuzzier.actions.grep.FuzzyGrep.Companion.MAX_OUTPUT_SIZE import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.RowContainer +import com.mituuz.fuzzier.ui.DefaultPopupProvider +import com.mituuz.fuzzier.ui.PopupConfig import kotlinx.coroutines.* import org.apache.commons.lang3.StringUtils import java.awt.event.ActionEvent @@ -62,7 +63,7 @@ import javax.swing.DefaultListModel import javax.swing.JComponent import javax.swing.KeyStroke -open class FuzzyGrep() : FuzzyAction() { +open class FuzzyGrep : FuzzyAction() { companion object { const val FUZZIER_NOTIFICATION_GROUP: String = "Fuzzier Notification Group" @@ -78,6 +79,7 @@ open class FuzzyGrep() : FuzzyAction() { var useRg = true val isWindows = System.getProperty("os.name").lowercase().contains("win") private var currentLaunchJob: Job? = null + private val popupProvider = DefaultPopupProvider() override fun runAction( project: Project, @@ -138,7 +140,20 @@ open class FuzzyGrep() : FuzzyAction() { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project, showSecondaryField = useRg) createListeners(project) - showPopup(project) + popup = popupProvider.show( + project = project, + content = component, + focus = component.searchField, + config = PopupConfig( + title = popupTitle, + preferredSizeProvider = component.preferredSize, + dimensionKey = dimensionKey, + resetWindow = { globalState.resetWindow }, + clearResetWindowFlag = { globalState.resetWindow = false } + ) + ) + + createPopup() createSharedListeners(project) (component as FuzzyFinderComponent).splitPane.dividerLocation = @@ -161,9 +176,7 @@ open class FuzzyGrep() : FuzzyAction() { Notifications.Bus.notify(grepNotification, project) } - override fun createPopup(screenDimensionKey: String): JBPopup { - val popup = getInitialPopup(screenDimensionKey) - + fun createPopup() { popup.addListener(object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { globalState.splitPosition = @@ -180,8 +193,6 @@ open class FuzzyGrep() : FuzzyAction() { actionScope?.cancel() } }) - - return popup } /** diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt new file mode 100644 index 00000000..3fdb9b09 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt @@ -0,0 +1,69 @@ +/* + * 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 + +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 : PopupProvider { + override fun show( + project: com.intellij.openapi.project.Project, + content: JComponent, + focus: JComponent, + config: PopupConfig + ): JBPopup { + // TODO: Check the error handling here + val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component + ?: error("No IDE frame found for project") + + val screenBounds = mainWindow.graphicsConfiguration.bounds + val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds) + + if (config.resetWindow()) { + DimensionService.getInstance().setSize(screenDimensionKey, config.preferredSizeProvider, null) + DimensionService.getInstance().setLocation(screenDimensionKey, null, null) + config.clearResetWindowFlag() + } + + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(content, focus) + .setFocusable(true) + .setRequestFocus(true) + .setResizable(true) + .setDimensionServiceKey(null, screenDimensionKey, true) + .setTitle(config.title) + .setMovable(true) + .setShowBorder(true) + .createPopup() + + popup.showInCenterOf(mainWindow) + return popup + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt new file mode 100644 index 00000000..62c80e6d --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt @@ -0,0 +1,47 @@ +/* + * 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 + +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, + ): JBPopup? +} \ No newline at end of file From cd05cd14701c01eba7aed986b526140f0754f8ee Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 06:12:57 +0200 Subject: [PATCH 34/61] Add another popup provider, which uses percentage of window --- .../fuzzier/ui/AutoSizePopupProvider.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt new file mode 100644 index 00000000..05028f3f --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt @@ -0,0 +1,75 @@ +/* + * 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 + +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 java.awt.Dimension +import javax.swing.JComponent + +class AutoSizePopupProvider : PopupProvider { + override fun show( + project: Project, + content: JComponent, + focus: JComponent, + config: PopupConfig + ): JBPopup { + // TODO: Check the error handling here + val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component + ?: error("No IDE frame found for project") + + val windowWidth = (mainWindow.width * 0.8).toInt() + val windowHeight = (mainWindow.height * 0.8).toInt() + val popupSize = Dimension(windowWidth, windowHeight) + + val screenBounds = mainWindow.graphicsConfiguration.bounds + val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds) + + if (config.resetWindow()) { + DimensionService.getInstance().setSize(screenDimensionKey, config.preferredSizeProvider, null) + DimensionService.getInstance().setLocation(screenDimensionKey, null, null) + config.clearResetWindowFlag() + } + + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(content, focus) + .setFocusable(true) + .setRequestFocus(true) + .setResizable(true) + .setTitle(config.title) + .setMovable(true) + .setShowBorder(true) + .createPopup() + + popup.size = popupSize + popup.showInCenterOf(mainWindow) + return popup + } +} \ No newline at end of file From 603b23052b639b91ac5158e267472bed0638cc1f Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 06:30:08 +0200 Subject: [PATCH 35/61] Simplify action registering for list selection --- .../fuzzier/actions/filesystem/Fuzzier.kt | 53 ++++---------- .../fuzzier/actions/filesystem/FuzzyMover.kt | 42 +++-------- .../mituuz/fuzzier/actions/grep/FuzzyGrep.kt | 53 ++++---------- .../fuzzier/ui/bindings/ActivationBindings.kt | 69 +++++++++++++++++++ .../ui/{ => popup}/AutoSizePopupProvider.kt | 2 +- .../ui/{ => popup}/DefaultPopupProvider.kt | 5 +- .../fuzzier/ui/{ => popup}/PopupProvider.kt | 2 +- 7 files changed, 112 insertions(+), 114 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindings.kt rename src/main/kotlin/com/mituuz/fuzzier/ui/{ => popup}/AutoSizePopupProvider.kt (98%) rename src/main/kotlin/com/mituuz/fuzzier/ui/{ => popup}/DefaultPopupProvider.kt (96%) rename src/main/kotlin/com/mituuz/fuzzier/ui/{ => popup}/PopupProvider.kt (97%) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index 69eefedc..a7d928dd 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -38,21 +38,15 @@ import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService -import com.mituuz.fuzzier.ui.DefaultPopupProvider -import com.mituuz.fuzzier.ui.PopupConfig +import com.mituuz.fuzzier.ui.bindings.ActivationBindings +import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider +import com.mituuz.fuzzier.ui.popup.PopupConfig import com.mituuz.fuzzier.util.InitialViewHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -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 open class Fuzzier : FilesystemAction() { override var popupTitle = "Fuzzy Search" @@ -140,36 +134,19 @@ open class Fuzzier : FilesystemAction() { } } - // 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) - } - } - } - }) + ActivationBindings.install( + component, + onActivate = { handleInput(project) } + ) + } - // 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) - } - } - }) + 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 createInitialView(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index fd381a4b..c1f7647b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -40,21 +40,15 @@ import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.mituuz.fuzzier.components.SimpleFinderComponent -import com.mituuz.fuzzier.ui.DefaultPopupProvider -import com.mituuz.fuzzier.ui.PopupConfig +import com.mituuz.fuzzier.ui.bindings.ActivationBindings +import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider +import com.mituuz.fuzzier.ui.popup.PopupConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -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 javax.swing.AbstractAction import javax.swing.DefaultListModel -import javax.swing.JComponent -import javax.swing.KeyStroke class FuzzyMover : FilesystemAction() { override var popupTitle = "Fuzzy File Mover" @@ -120,28 +114,13 @@ class FuzzyMover : FilesystemAction() { } 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 @@ -159,7 +138,6 @@ class FuzzyMover : FilesystemAction() { component.isDirSelector = true component.searchField.text = "" component.fileList.setEmptyText("Select target folder") - completableFuture.complete(null) } } else { ApplicationManager.getApplication().invokeLater { @@ -187,13 +165,9 @@ class FuzzyMover : FilesystemAction() { ApplicationManager.getApplication().invokeLater { popup.cancel() } - completableFuture.complete(null) - } else { - completableFuture.complete(null) } } } } - return completableFuture } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt index df2772b1..e04b9711 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt @@ -50,18 +50,12 @@ import com.mituuz.fuzzier.actions.grep.FuzzyGrep.Companion.MAX_OUTPUT_SIZE import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.RowContainer -import com.mituuz.fuzzier.ui.DefaultPopupProvider -import com.mituuz.fuzzier.ui.PopupConfig +import com.mituuz.fuzzier.ui.bindings.ActivationBindings +import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider +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 open class FuzzyGrep : FuzzyAction() { companion object { @@ -374,36 +368,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) - } - } - } - }) + ActivationBindings.install( + component, + onActivate = { handleInput(project) } + ) + } - // 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) - } - } - }) + 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/ui/bindings/ActivationBindings.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindings.kt new file mode 100644 index 00000000..f9b42577 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindings.kt @@ -0,0 +1,69 @@ +/* + * 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.mituuz.fuzzier.components.FuzzyComponent +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.JComponent +import javax.swing.KeyStroke + +object ActivationBindings { + fun install( + component: FuzzyComponent, + onActivate: () -> 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/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt rename to src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt index 05028f3f..0045085f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/AutoSizePopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.ui +package com.mituuz.fuzzier.ui.popup import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt similarity index 96% rename from src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt rename to src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt index 3fdb9b09..efb0e47d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/DefaultPopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt @@ -22,8 +22,9 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.ui +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 @@ -34,7 +35,7 @@ import javax.swing.JComponent class DefaultPopupProvider : PopupProvider { override fun show( - project: com.intellij.openapi.project.Project, + project: Project, content: JComponent, focus: JComponent, config: PopupConfig diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt similarity index 97% rename from src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt rename to src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt index 62c80e6d..d0c8cefa 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/PopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.ui +package com.mituuz.fuzzier.ui.popup import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup From 60e14b6a87046bc218a6cdf6fbdda307adbc11ae Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:07:40 +0200 Subject: [PATCH 36/61] Handle common popup closing using the base class --- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 14 ++++++++++ .../fuzzier/actions/filesystem/Fuzzier.kt | 28 +++++-------------- .../fuzzier/actions/filesystem/FuzzyMover.kt | 21 ++------------ .../mituuz/fuzzier/actions/grep/FuzzyGrep.kt | 27 +++++------------- .../fuzzier/ui/popup/AutoSizePopupProvider.kt | 12 +++++++- .../fuzzier/ui/popup/DefaultPopupProvider.kt | 12 +++++++- .../mituuz/fuzzier/ui/popup/PopupProvider.kt | 1 + 7 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 3b2399f1..0b961eb3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -48,6 +48,7 @@ import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import java.awt.Component import java.awt.Font import java.awt.event.ActionEvent @@ -141,6 +142,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/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index a7d928dd..8f94ac6d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -30,8 +30,6 @@ import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.EditorHistoryManager import com.intellij.openapi.project.Project -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.util.SingleAlarm @@ -79,9 +77,9 @@ open class Fuzzier : FilesystemAction() { dimensionKey = dimensionKey, resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false } - ) + ), + cleanupFunction = { cleanupPopup() } ) - createPopup() createSharedListeners(project) @@ -94,23 +92,11 @@ open class Fuzzier : FilesystemAction() { } } - fun createPopup() { - popup.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - globalState.splitPosition = - (component as FuzzyFinderComponent).splitPane.dividerLocation - - resetOriginalHandlers() - - currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = null - - actionScope?.cancel() - - previewAlarm?.dispose() - lastPreviewKey = null - } - }) + override fun onPopupClosed() { + globalState.splitPosition = + (component as FuzzyFinderComponent).splitPane.dividerLocation + previewAlarm?.dispose() + lastPreviewKey = null } override fun handleEmptySearchString(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index c1f7647b..8a1c5c7b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -32,8 +32,6 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project -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 @@ -76,7 +74,6 @@ class FuzzyMover : FilesystemAction() { component.fileList.setEmptyText("Press enter to use current file: ${currentFile.path}") } - // showPopup(project) popup = popupProvider.show( project = project, content = component, @@ -87,26 +84,14 @@ class FuzzyMover : FilesystemAction() { dimensionKey = dimensionKey, resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false } - ) + ), + cleanupFunction = { cleanupPopup() }, ) - createPopup() + createSharedListeners(project) } } - fun createPopup() { - popup.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - resetOriginalHandlers() - - currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = null - - actionScope?.cancel() - } - }) - } - override fun handleEmptySearchString(project: Project) { ApplicationManager.getApplication().invokeLater { component.fileList.model = DefaultListModel() diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt index e04b9711..4c9df40e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt @@ -39,8 +39,6 @@ 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.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 @@ -144,10 +142,10 @@ open class FuzzyGrep : FuzzyAction() { dimensionKey = dimensionKey, resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false } - ) + ), + cleanupFunction = { cleanupPopup() }, ) - createPopup() createSharedListeners(project) (component as FuzzyFinderComponent).splitPane.dividerLocation = @@ -170,23 +168,12 @@ open class FuzzyGrep : FuzzyAction() { Notifications.Bus.notify(grepNotification, project) } - fun createPopup() { - popup.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - globalState.splitPosition = - (component as FuzzyFinderComponent).splitPane.dividerLocation - - resetOriginalHandlers() - - currentLaunchJob?.cancel() - currentLaunchJob = null + override fun onPopupClosed() { + globalState.splitPosition = + (component as FuzzyFinderComponent).splitPane.dividerLocation - currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = null - - actionScope?.cancel() - } - }) + currentLaunchJob?.cancel() + currentLaunchJob = null } /** diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt index 0045085f..c2174e8f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt @@ -27,6 +27,8 @@ 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.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.DimensionService import com.intellij.openapi.wm.WindowManager import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey @@ -39,7 +41,8 @@ class AutoSizePopupProvider : PopupProvider { project: Project, content: JComponent, focus: JComponent, - config: PopupConfig + config: PopupConfig, + cleanupFunction: () -> Unit, ): JBPopup { // TODO: Check the error handling here val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component @@ -70,6 +73,13 @@ class AutoSizePopupProvider : PopupProvider { popup.size = popupSize popup.showInCenterOf(mainWindow) + + popup.addListener(object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + cleanupFunction() + } + }) + return popup } } \ 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 index efb0e47d..a71c19cf 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt @@ -27,6 +27,8 @@ 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.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.DimensionService import com.intellij.openapi.wm.WindowManager import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey @@ -38,7 +40,8 @@ class DefaultPopupProvider : PopupProvider { project: Project, content: JComponent, focus: JComponent, - config: PopupConfig + config: PopupConfig, + cleanupFunction: () -> Unit, ): JBPopup { // TODO: Check the error handling here val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component @@ -65,6 +68,13 @@ class DefaultPopupProvider : PopupProvider { .createPopup() popup.showInCenterOf(mainWindow) + + popup.addListener(object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + 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 index d0c8cefa..eac1c25c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProvider.kt @@ -43,5 +43,6 @@ interface PopupProvider { content: JComponent, focus: JComponent, config: PopupConfig, + cleanupFunction: () -> Unit ): JBPopup? } \ No newline at end of file From e71fa0f9a4332a4be129b0dff022a14600512ce7 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:20:46 +0200 Subject: [PATCH 37/61] Remove unnecessary async handling and fix tests --- .../fuzzier/actions/filesystem/FuzzyMover.kt | 47 ++++++-------- .../com/mituuz/fuzzier/FuzzyActionTest.kt | 5 -- .../com/mituuz/fuzzier/FuzzyMoverTest.kt | 61 +++++++++++-------- 3 files changed, 53 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index 8a1c5c7b..f3d69b06 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import java.util.concurrent.CompletableFuture import javax.swing.DefaultListModel class FuzzyMover : FilesystemAction() { @@ -83,8 +82,7 @@ class FuzzyMover : FilesystemAction() { preferredSizeProvider = component.preferredSize, dimensionKey = dimensionKey, resetWindow = { globalState.resetWindow }, - clearResetWindowFlag = { globalState.resetWindow = false } - ), + clearResetWindowFlag = { globalState.resetWindow = false }), cleanupFunction = { cleanupPopup() }, ) @@ -100,9 +98,7 @@ class FuzzyMover : FilesystemAction() { private fun createListeners(project: Project) { ActivationBindings.install( - component, - onActivate = { handleInput(project) } - ) + component, onActivate = { handleInput(project) }) } fun handleInput(project: Project) { @@ -126,30 +122,25 @@ class FuzzyMover : FilesystemAction() { } } 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() - } + 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() } } } diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt index b1a5ab09..8841005b 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt @@ -28,7 +28,6 @@ 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 @@ -193,10 +192,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 e55e02d6..00d7afd9 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt @@ -24,6 +24,7 @@ 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 @@ -31,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.actions.filesystem.FuzzyMover import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer @@ -68,12 +71,12 @@ class FuzzyMoverTest { 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() } @@ -94,16 +97,15 @@ class FuzzyMoverTest { 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() } @@ -129,16 +131,15 @@ class FuzzyMoverTest { 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() } @@ -163,22 +164,28 @@ 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() + 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) { From ae5cb64c8e217788d7383449fb2467ee7fed875f Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:27:33 +0200 Subject: [PATCH 38/61] Update changelog --- changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index d1288d15..1422bf6c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## Version 1.15.0 + +- Refactor fuzzy 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 + ## Version 1.14.0 - Add a global exclusion list for convenience when working with multiple projects From bc393684858751d8b00286b03d32803f9948366b Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:27:48 +0200 Subject: [PATCH 39/61] Increment version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9bc2fa3e..cc7c4bfb 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 From be68ec388b368439d43b0567de0645507184cb36 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:28:06 +0200 Subject: [PATCH 40/61] Move common calls to base class --- .../kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt | 7 ++++--- .../com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt | 9 --------- .../com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt | 9 --------- .../kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt | 4 ---- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 0b961eb3..a5e2e6e0 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -46,9 +46,7 @@ import com.mituuz.fuzzier.entities.FuzzyContainer.FilenameType import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel +import kotlinx.coroutines.* import java.awt.Component import java.awt.Font import java.awt.event.ActionEvent @@ -80,6 +78,9 @@ abstract class FuzzyAction : AnAction() { if (project != null) { projectState = project.service().state fuzzierUtil.parseModules(project) + setCustomHandlers() + actionScope?.cancel() + actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) runAction(project, actionEvent) } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt index 8f94ac6d..1101bd0d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt @@ -40,10 +40,6 @@ import com.mituuz.fuzzier.ui.bindings.ActivationBindings import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider import com.mituuz.fuzzier.ui.popup.PopupConfig import com.mituuz.fuzzier.util.InitialViewHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import javax.swing.DefaultListModel open class Fuzzier : FilesystemAction() { @@ -57,11 +53,6 @@ open class Fuzzier : FilesystemAction() { { vf -> !vf.isDirectory } override fun runAction(project: Project, actionEvent: AnActionEvent) { - setCustomHandlers() - - actionScope?.cancel() - actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - ApplicationManager.getApplication().invokeLater { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt index f3d69b06..5fcb5b8c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt @@ -41,10 +41,6 @@ import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.ui.bindings.ActivationBindings import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider import com.mituuz.fuzzier.ui.popup.PopupConfig -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import javax.swing.DefaultListModel class FuzzyMover : FilesystemAction() { @@ -59,11 +55,6 @@ class FuzzyMover : FilesystemAction() { } override fun runAction(project: Project, actionEvent: AnActionEvent) { - setCustomHandlers() - - actionScope?.cancel() - actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - ApplicationManager.getApplication().invokeLater { component = SimpleFinderComponent() createListeners(project) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt index 4c9df40e..4d72140c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt @@ -78,10 +78,6 @@ open class FuzzyGrep : FuzzyAction() { actionEvent: AnActionEvent ) { currentLaunchJob?.cancel() - setCustomHandlers() - - actionScope?.cancel() - actionScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val projectBasePath = project.basePath.toString() currentLaunchJob = actionScope?.launch(Dispatchers.EDT) { From daae0df7b08db428bec4eca2b7b8750958690a40 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:39:29 +0200 Subject: [PATCH 41/61] Dont break existing keybindings --- .../{actions/filesystem => }/Fuzzier.kt | 3 ++- .../{actions/filesystem => }/FuzzierVCS.kt | 2 +- .../fuzzier/{actions/grep => }/FuzzyGrep.kt | 6 +++--- .../grep => }/FuzzyGrepCaseInsensitive.kt | 2 +- .../{actions/filesystem => }/FuzzyMover.kt | 3 ++- src/main/resources/META-INF/plugin.xml | 20 +++++++++---------- .../kotlin/com/mituuz/fuzzier/FuzzierTest.kt | 1 - .../com/mituuz/fuzzier/FuzzyMoverTest.kt | 1 - 8 files changed, 19 insertions(+), 19 deletions(-) rename src/main/kotlin/com/mituuz/fuzzier/{actions/filesystem => }/Fuzzier.kt (98%) rename src/main/kotlin/com/mituuz/fuzzier/{actions/filesystem => }/FuzzierVCS.kt (97%) rename src/main/kotlin/com/mituuz/fuzzier/{actions/grep => }/FuzzyGrep.kt (98%) rename src/main/kotlin/com/mituuz/fuzzier/{actions/grep => }/FuzzyGrepCaseInsensitive.kt (98%) rename src/main/kotlin/com/mituuz/fuzzier/{actions/filesystem => }/FuzzyMover.kt (98%) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt rename to src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 1101bd0d..434c9b0d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.actions.filesystem +package com.mituuz.fuzzier import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager @@ -33,6 +33,7 @@ import com.intellij.openapi.project.Project 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.settings.FuzzierGlobalSettingsService diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt similarity index 97% rename from src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt rename to src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt index 0a0a17bb..a351cc87 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzierVCS.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzierVCS.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.actions.filesystem +package com.mituuz.fuzzier import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.changes.ChangeListManager diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt rename to src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 4d72140c..0f381fbd 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.actions.grep +package com.mituuz.fuzzier import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.OSProcessHandler @@ -42,9 +42,9 @@ import com.intellij.openapi.project.Project 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.actions.grep.FuzzyGrep.Companion.MAX_NUMBER_OR_RESULTS -import com.mituuz.fuzzier.actions.grep.FuzzyGrep.Companion.MAX_OUTPUT_SIZE import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.RowContainer diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt rename to src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt index d1d288d1..7cc02f54 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/grep/FuzzyGrepCaseInsensitive.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.actions.grep +package com.mituuz.fuzzier import com.mituuz.fuzzier.entities.FuzzyContainer import javax.swing.DefaultListModel diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt similarity index 98% rename from src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt rename to src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index 5fcb5b8c..5bb6710a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.mituuz.fuzzier.actions.filesystem +package com.mituuz.fuzzier import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -37,6 +37,7 @@ 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.ui.bindings.ActivationBindings import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index fb1af69c..5755d0d7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -65,35 +65,35 @@ - - diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt index 64fe4e24..35939958 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzierTest.kt @@ -24,7 +24,6 @@ package com.mituuz.fuzzier import com.intellij.testFramework.TestApplicationManager -import com.mituuz.fuzzier.actions.filesystem.Fuzzier import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertFalse diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt index 00d7afd9..013cf974 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt @@ -36,7 +36,6 @@ 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.actions.filesystem.FuzzyMover import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer From e7056c341d44a3c5940ea62a8731aac6bd9769c0 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:50:16 +0200 Subject: [PATCH 42/61] Remove inherited fields --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 6 ++---- src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 5 ++--- src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt | 6 ++---- src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt | 2 -- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 434c9b0d..f327630a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -44,8 +44,6 @@ import com.mituuz.fuzzier.util.InitialViewHandler import javax.swing.DefaultListModel open class Fuzzier : FilesystemAction() { - override var popupTitle = "Fuzzy Search" - override var dimensionKey = "FuzzySearchPopup" private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null private val popupProvider = DefaultPopupProvider() @@ -64,9 +62,9 @@ open class Fuzzier : FilesystemAction() { content = component, focus = component.searchField, config = PopupConfig( - title = popupTitle, + title = "Fuzzy Search", preferredSizeProvider = component.preferredSize, - dimensionKey = dimensionKey, + dimensionKey = "FuzzySearchPopup", resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false } ), diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 0f381fbd..7008d9e5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -66,8 +66,6 @@ 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 @@ -82,6 +80,7 @@ open class FuzzyGrep : FuzzyAction() { val projectBasePath = project.basePath.toString() currentLaunchJob = actionScope?.launch(Dispatchers.EDT) { val currentJob = currentLaunchJob + var popupTitle = "Fuzzy Grep" if (!isInstalled("rg", projectBasePath)) { showNotification( @@ -135,7 +134,7 @@ open class FuzzyGrep : FuzzyAction() { config = PopupConfig( title = popupTitle, preferredSizeProvider = component.preferredSize, - dimensionKey = dimensionKey, + dimensionKey = "FuzzyGrepPopup", resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false } ), diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index 5bb6710a..f588a200 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -45,8 +45,6 @@ import com.mituuz.fuzzier.ui.popup.PopupConfig import javax.swing.DefaultListModel class FuzzyMover : FilesystemAction() { - override var popupTitle = "Fuzzy File Mover" - override var dimensionKey = "FuzzyMoverPopup" lateinit var movableFile: PsiFile lateinit var currentFile: VirtualFile private val popupProvider = DefaultPopupProvider() @@ -70,9 +68,9 @@ class FuzzyMover : FilesystemAction() { content = component, focus = component.searchField, config = PopupConfig( - title = popupTitle, + title = "Fuzzy File Mover", preferredSizeProvider = component.preferredSize, - dimensionKey = dimensionKey, + dimensionKey = "FuzzyMoverPopup", resetWindow = { globalState.resetWindow }, clearResetWindowFlag = { globalState.resetWindow = false }), cleanupFunction = { cleanupPopup() }, diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index a5e2e6e0..75c7147e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -57,8 +57,6 @@ 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 From b8b867624a46ce223d2016a94b7ceb678935d7f4 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 07:59:29 +0200 Subject: [PATCH 43/61] Simplify fuzzy grep and fix missing overrides --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 3 +- .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 25 ++++++----------- .../fuzzier/FuzzyGrepCaseInsensitive.kt | 1 - .../mituuz/fuzzier/search/ResultsProvider.kt | 28 +++++++++++++++++++ 4 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/search/ResultsProvider.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index f327630a..ba308bd2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -47,6 +47,7 @@ open class Fuzzier : FilesystemAction() { private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null private val popupProvider = DefaultPopupProvider() + protected open var popupTitle = "Fuzzy Search" override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = { vf -> !vf.isDirectory } @@ -62,7 +63,7 @@ open class Fuzzier : FilesystemAction() { content = component, focus = component.searchField, config = PopupConfig( - title = "Fuzzy Search", + title = popupTitle, preferredSizeProvider = component.preferredSize, dimensionKey = "FuzzySearchPopup", resetWindow = { globalState.resetWindow }, diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 7008d9e5..c6af3f1f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -54,6 +54,7 @@ import com.mituuz.fuzzier.ui.popup.PopupConfig import kotlinx.coroutines.* import org.apache.commons.lang3.StringUtils import javax.swing.DefaultListModel +import javax.swing.ListModel open class FuzzyGrep : FuzzyAction() { companion object { @@ -70,6 +71,7 @@ open class FuzzyGrep : FuzzyAction() { val isWindows = System.getProperty("os.name").lowercase().contains("win") private var currentLaunchJob: Job? = null private val popupProvider = DefaultPopupProvider() + protected open lateinit var popupTitle: String override fun runAction( project: Project, @@ -80,7 +82,6 @@ open class FuzzyGrep : FuzzyAction() { val projectBasePath = project.basePath.toString() currentLaunchJob = actionScope?.launch(Dispatchers.EDT) { val currentJob = currentLaunchJob - var popupTitle = "Fuzzy Grep" if (!isInstalled("rg", projectBasePath)) { showNotification( @@ -197,21 +198,10 @@ open class FuzzyGrep : FuzzyAction() { currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { component.fileList.setPaintBusy(true) try { - val currentJob = currentUpdateListContentJob - - if (currentJob?.isCancelled == true) return@launch - - val listModel = DefaultListModel() - - if (currentJob?.isCancelled == true) return@launch - val results = withContext(Dispatchers.IO) { - findInFiles(searchString, listModel, project.basePath.toString()) - listModel + findInFiles(searchString, project.basePath.toString()) } - - if (currentJob?.isCancelled == true) return@launch - + coroutineContext.ensureActive() component.refreshModel(results, getCellRenderer()) } finally { component.fileList.setPaintBusy(false) @@ -293,9 +283,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( @@ -320,6 +311,8 @@ open class FuzzyGrep : FuzzyAction() { runCommand(listOf("grep", "--color=none", "-r", "-n", searchString, "."), listModel, projectBasePath) } } + + return listModel } private fun createListeners(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt index 7cc02f54..cef42f09 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrepCaseInsensitive.kt @@ -29,7 +29,6 @@ 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, 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 From dc4b242a950864d6d9b813874a4f5c91b62b5b0b Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 08:01:23 +0200 Subject: [PATCH 44/61] Remove unnecessary imports --- src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index c6af3f1f..12daefe3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -42,8 +42,6 @@ import com.intellij.openapi.project.Project 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 From 8a4860aaf1f49f62bbbada8c2c63abdd02c0176d Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 09:13:32 +0200 Subject: [PATCH 45/61] Refactor test bench --- .../actions/filesystem/FilesystemAction.kt | 27 ++- .../fuzzier/components/TestBenchComponent.kt | 223 ++++++++++++------ .../fuzzier/entities/StringEvaluator.kt | 46 ++-- ...deIgnoreTest.kt => StringEvaluatorTest.kt} | 2 +- .../kotlin/com/mituuz/fuzzier/TestUtil.kt | 8 - 5 files changed, 183 insertions(+), 123 deletions(-) rename src/test/kotlin/com/mituuz/fuzzier/{ExcludeIgnoreTest.kt => StringEvaluatorTest.kt} (99%) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index e30d5d15..e0253e88 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -93,11 +93,13 @@ abstract class FilesystemAction : FuzzyAction() { suspend fun processIterationEntries( fileEntries: List, stringEvaluator: StringEvaluator, - searchString: String + searchString: String, + fileListLimit: Int, + ignoredChars: String ): DefaultListModel { - val ss = FuzzierUtil.Companion.cleanSearchString(searchString, projectState.ignoredCharacters) + val ss = FuzzierUtil.cleanSearchString(searchString, ignoredChars) val processedFiles = ConcurrentHashMap.newKeySet() - val listLimit = globalState.fileListLimit + val listLimit = fileListLimit val priorityQueue = PriorityQueue( listLimit + 1, compareBy { it.getScore() } @@ -118,7 +120,11 @@ abstract class FilesystemAction : FuzzyAction() { val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss) container?.let { fuzzyMatchContainer -> synchronized(queueLock) { - minimumScore = priorityQueue.maybeAdd(minimumScore, fuzzyMatchContainer) + minimumScore = priorityQueue.maybeAdd( + minimumScore, + fuzzyMatchContainer, + fileListLimit + ) } } } @@ -142,13 +148,14 @@ abstract class FilesystemAction : FuzzyAction() { private fun PriorityQueue.maybeAdd( minimumScore: Int?, - fuzzyMatchContainer: FuzzyMatchContainer + fuzzyMatchContainer: FuzzyMatchContainer, + fileListLimit: Int, ): Int? { var ret = minimumScore if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { this.add(fuzzyMatchContainer) - if (this.size > globalState.fileListLimit) { + if (this.size > fileListLimit) { this.remove() ret = this.peek().getScore() } @@ -177,7 +184,13 @@ abstract class FilesystemAction : FuzzyAction() { coroutineContext.ensureActive() val listModel = withContext(Dispatchers.Default) { - processIterationEntries(iterationEntries, stringEvaluator, searchString) + processIterationEntries( + iterationEntries, + stringEvaluator, + searchString, + globalState.fileListLimit, + projectState.ignoredCharacters, + ) } coroutineContext.ensureActive() diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt index 91e4336c..2a2a6ba3 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] @@ -148,85 +155,48 @@ class TestBenchComponent : JPanel(), Disposable { 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 listModel = withContext(Dispatchers.Default) { + processIterationEntries( + iterationEntries, + stringEvaluator, + searchString, + liveSettingsComponent.fileListLimit.getIntSpinner().value as Int, + ) + } - 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() } + 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 +217,105 @@ 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, + ): DefaultListModel { + val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) + val processedFiles = ConcurrentHashMap.newKeySet() + val priorityQueue = PriorityQueue( + fileListLimit + 1, + compareBy { it.getScore() } + ) + + 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 scoreCalculator = ScoreCalculator(ss) + 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) + + val container = stringEvaluator.evaluateIteratorEntry(iterationFile, scoreCalculator) + container?.let { fuzzyMatchContainer -> + synchronized(queueLock) { + minimumScore = priorityQueue.maybeAdd( + minimumScore, + fuzzyMatchContainer, + fileListLimit + ) + } + } + } + } + } + + fileEntries + .filter { processedFiles.add(it.path) } + .forEach { ch.send(it) } + ch.close() + } + + + val result = DefaultListModel() + result.addAll( + priorityQueue.sortedWith( + compareByDescending { it.getScore() }) + ) + return result + } + + private fun PriorityQueue.maybeAdd( + minimumScore: Int?, + fuzzyMatchContainer: FuzzyMatchContainer, + fileListLimit: Int + ): Int? { + var ret = minimumScore + + if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + this.add(fuzzyMatchContainer) + if (this.size > fileListLimit) { + this.remove() + ret = this.peek().getScore() + } + } + + return ret + } } \ 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 20558fa2..551cbac3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -23,11 +23,6 @@ */ package com.mituuz.fuzzier.entities -import com.intellij.openapi.roots.ContentIterator -import com.intellij.openapi.vfs.VirtualFile -import java.util.concurrent.Future -import javax.swing.DefaultListModel - /** * Handles creating the content iterators used for string handling and excluding files * @param exclusionList exclusion list from settings @@ -36,37 +31,26 @@ class StringEvaluator( private var exclusionList: Set, private var modules: Map, ) { - lateinit var scoreCalculator: ScoreCalculator + fun evaluateIteratorEntry(iteratorEntry: IterationEntry, searchString: String): FuzzyMatchContainer? { + val scoreCalculator = ScoreCalculator(searchString) + val moduleName = iteratorEntry.module - 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 moduleBasePath = modules[moduleName] ?: return null - val filePath = file.path.removePrefix(moduleBasePath) - if (isExcluded(filePath)) { - return@ContentIterator true - } - if (filePath.isNotBlank()) { - val fuzzyMatchContainer = createFuzzyContainer(filePath, moduleBasePath, scoreCalculator) - if (fuzzyMatchContainer != null) { - listModel.addElement(fuzzyMatchContainer) - } - } - } - true + val dirPath = iteratorEntry.path.removePrefix(moduleBasePath) + if (isExcluded(dirPath)) return null + + if (dirPath.isNotBlank()) { + return createFuzzyContainer(dirPath, moduleBasePath, scoreCalculator) } + + return null } - fun evaluateIteratorEntry(iteratorEntry: IterationEntry, searchString: String): FuzzyMatchContainer? { - val scoreCalculator = ScoreCalculator(searchString) + fun evaluateIteratorEntry( + iteratorEntry: IterationEntry, + scoreCalculator: ScoreCalculator, + ): FuzzyMatchContainer? { val moduleName = iteratorEntry.module val moduleBasePath = modules[moduleName] ?: return null diff --git a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt similarity index 99% rename from src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt rename to src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt index e879ee5e..c8642653 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/ExcludeIgnoreTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt @@ -26,7 +26,7 @@ package com.mituuz.fuzzier import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -class ExcludeIgnoreTest { +class StringEvaluatorTest { private var testUtil = TestUtil() @Test diff --git a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt index 05a93caa..6c8722ca 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt @@ -36,7 +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 javax.swing.DefaultListModel class TestUtil { @@ -80,13 +79,6 @@ class TestUtil { val module = myFixture.project.modules[0] map[module.name] = module.rootManager.contentRoots[1].path - val 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 From 143b3fbc52d3fda5406cf48d098acdab45233273 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 09:22:54 +0200 Subject: [PATCH 46/61] Update string ev tests to match the new implementation --- .../fuzzier/entities/StringEvaluator.kt | 3 + .../com/mituuz/fuzzier/StringEvaluatorTest.kt | 58 +++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 551cbac3..4fa438d9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -47,6 +47,9 @@ class StringEvaluator( return null } + /** + * Only used by test bench as it provides it's own custom scoreCalculator + */ fun evaluateIteratorEntry( iteratorEntry: IterationEntry, scoreCalculator: ScoreCalculator, diff --git a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt index c8642653..90b098fb 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt @@ -23,63 +23,71 @@ */ package com.mituuz.fuzzier +import com.intellij.testFramework.TestApplicationManager +import com.mituuz.fuzzier.entities.IterationEntry +import com.mituuz.fuzzier.entities.StringEvaluator import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class StringEvaluatorTest { - private var testUtil = TestUtil() + @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) + evaluator.evaluateIteratorEntry(entry, "")?.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 filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("asd", "nope")) - Assertions.assertEquals(1, filePathContainer.size()) - Assertions.assertEquals("/main.kt", filePathContainer.get(0).filePath) + 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 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) + 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 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) + 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 filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("/asd*")) - Assertions.assertEquals(2, filePathContainer.size()) - Assertions.assertEquals("/not/asd.kt", filePathContainer.get(0).filePath) + 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 filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf("*.log")) - Assertions.assertEquals(3, filePathContainer.size()) - Assertions.assertEquals("/asd/asd.kt", filePathContainer.get(0).filePath) + 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 filePathContainer = testUtil.setUpModuleFileIndex(filePaths, setOf()) - 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) + val results = evaluate(filePaths, emptySet()) + Assertions.assertEquals(setOf("/dir/file.txt", "/main.kt", "/other.kt"), results.toSet()) } } \ No newline at end of file From ac5d17eb56a12a195e2f3c93723c6f4c011dce92 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 09:52:20 +0200 Subject: [PATCH 47/61] Remove state linking for score calculator --- .../actions/filesystem/FilesystemAction.kt | 16 +- .../fuzzier/components/TestBenchComponent.kt | 59 ++--- .../mituuz/fuzzier/entities/MatchConfig.kt | 34 +++ .../fuzzier/entities/ScoreCalculator.kt | 100 +++----- .../fuzzier/entities/StringEvaluator.kt | 23 +- .../com/mituuz/fuzzier/StringEvaluatorTest.kt | 6 +- .../fuzzier/entities/ScoreCalculatorTest.kt | 237 +++++++++--------- 7 files changed, 231 insertions(+), 244 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/entities/MatchConfig.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index e0253e88..1038976a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -32,10 +32,7 @@ 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.FuzzyContainer -import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.IterationEntry -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.util.FuzzierUtil @@ -111,13 +108,22 @@ abstract class FilesystemAction : FuzzyAction() { 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) + val container = stringEvaluator.evaluateIteratorEntry(iterationFile, ss, matchConfig) container?.let { fuzzyMatchContainer -> synchronized(queueLock) { minimumScore = priorityQueue.maybeAdd( diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt index 2a2a6ba3..ce4a84a8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt @@ -84,8 +84,7 @@ class TestBenchComponent : JPanel(), Disposable { scrollPane.setViewportView(table) add( - scrollPane, - GridConstraints( + scrollPane, GridConstraints( 0, 0, 1, @@ -102,8 +101,7 @@ class TestBenchComponent : JPanel(), Disposable { ) ) add( - searchField, - GridConstraints( + searchField, GridConstraints( 1, 0, 1, @@ -144,11 +142,9 @@ 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) @@ -161,8 +157,7 @@ class TestBenchComponent : JPanel(), Disposable { try { val stringEvaluator = StringEvaluator( - combinedExclusions, - project.service().state.modules + combinedExclusions, project.service().state.modules ) val iterationEntries = withContext(Dispatchers.Default) { @@ -230,14 +225,11 @@ class TestBenchComponent : JPanel(), Disposable { } return collector.collectFiles( - targets = indexTargets, - shouldContinue = { job.isActive }, - fileFilter = buildFileFilter() + targets = indexTargets, shouldContinue = { job.isActive }, fileFilter = buildFileFilter() ) } - private fun buildFileFilter(): (VirtualFile) -> Boolean = - { vf -> !vf.isDirectory } + private fun buildFileFilter(): (VirtualFile) -> Boolean = { vf -> !vf.isDirectory } suspend fun processIterationEntries( fileEntries: List, @@ -248,9 +240,7 @@ class TestBenchComponent : JPanel(), Disposable { val ss = FuzzierUtil.cleanSearchString(searchString, projectState.ignoredCharacters) val processedFiles = ConcurrentHashMap.newKeySet() val priorityQueue = PriorityQueue( - fileListLimit + 1, - compareBy { it.getScore() } - ) + fileListLimit + 1, compareBy { it.getScore() }) val queueLock = Any() var minimumScore: Int? = null @@ -264,21 +254,20 @@ class TestBenchComponent : JPanel(), Disposable { repeat(parallelism) { launch { for (iterationFile in ch) { - val scoreCalculator = ScoreCalculator(ss) - 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) - - val container = stringEvaluator.evaluateIteratorEntry(iterationFile, scoreCalculator) + 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 + minimumScore, fuzzyMatchContainer, fileListLimit ) } } @@ -286,9 +275,7 @@ class TestBenchComponent : JPanel(), Disposable { } } - fileEntries - .filter { processedFiles.add(it.path) } - .forEach { ch.send(it) } + fileEntries.filter { processedFiles.add(it.path) }.forEach { ch.send(it) } ch.close() } @@ -302,9 +289,7 @@ class TestBenchComponent : JPanel(), Disposable { } private fun PriorityQueue.maybeAdd( - minimumScore: Int?, - fuzzyMatchContainer: FuzzyMatchContainer, - fileListLimit: Int + minimumScore: Int?, fuzzyMatchContainer: FuzzyMatchContainer, fileListLimit: Int ): Int? { var ret = minimumScore 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 4fa438d9..6c3ec09e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -31,29 +31,12 @@ class StringEvaluator( private var exclusionList: Set, private var modules: Map, ) { - fun evaluateIteratorEntry(iteratorEntry: IterationEntry, searchString: String): FuzzyMatchContainer? { - val scoreCalculator = ScoreCalculator(searchString) - val moduleName = iteratorEntry.module - - val moduleBasePath = modules[moduleName] ?: return null - - val dirPath = iteratorEntry.path.removePrefix(moduleBasePath) - if (isExcluded(dirPath)) return null - - if (dirPath.isNotBlank()) { - return createFuzzyContainer(dirPath, moduleBasePath, scoreCalculator) - } - - return null - } - - /** - * Only used by test bench as it provides it's own custom scoreCalculator - */ fun evaluateIteratorEntry( iteratorEntry: IterationEntry, - scoreCalculator: ScoreCalculator, + searchString: String, + matchConfig: MatchConfig ): FuzzyMatchContainer? { + val scoreCalculator = ScoreCalculator(searchString, matchConfig) val moduleName = iteratorEntry.module val moduleBasePath = modules[moduleName] ?: return null diff --git a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt index 90b098fb..3f8126c1 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt @@ -25,6 +25,7 @@ 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 @@ -45,7 +46,10 @@ class StringEvaluatorTest { val fullPath = "/m1/src$relativeToSrc" val name = fullPath.substring(fullPath.lastIndexOf('/') + 1) val entry = IterationEntry(name = name, path = fullPath, module = moduleName) - evaluator.evaluateIteratorEntry(entry, "")?.filePath + evaluator.evaluateIteratorEntry( + entry, "", + MatchConfig() + )?.filePath }.sorted() // deterministic order for assertions } 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")) } From 1d6d4261fcdecdb8860dd5e4d1d26ab22dd6a412 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 10:23:18 +0200 Subject: [PATCH 48/61] Fix shorter dir paths --- .../actions/filesystem/FilesystemAction.kt | 16 +- .../fuzzier/components/TestBenchComponent.kt | 22 +- .../fuzzier/entities/FuzzyMatchContainer.kt | 90 ++++---- .../mituuz/fuzzier/entities/IterationEntry.kt | 3 +- .../fuzzier/entities/StringEvaluator.kt | 16 +- .../IntelliJIterationFileCollector.kt | 2 +- .../com/mituuz/fuzzier/util/FuzzierUtil.kt | 65 ------ .../com/mituuz/fuzzier/FuzzyActionTest.kt | 15 +- .../com/mituuz/fuzzier/FuzzyMoverTest.kt | 8 +- .../com/mituuz/fuzzier/StringEvaluatorTest.kt | 2 +- .../entities/FuzzyMatchContainerTest.kt | 72 +++--- .../mituuz/fuzzier/util/FuzzierUtilTest.kt | 206 ++++-------------- .../fuzzier/util/InitialViewHandlerTest.kt | 11 +- 13 files changed, 187 insertions(+), 341 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt index 1038976a..94ba0f6f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/filesystem/FilesystemAction.kt @@ -92,14 +92,15 @@ abstract class FilesystemAction : FuzzyAction() { stringEvaluator: StringEvaluator, searchString: String, fileListLimit: Int, - ignoredChars: String + 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() } + compareBy { it.getScore(prioritizeShorterDirPaths) } ) val queueLock = Any() @@ -129,7 +130,8 @@ abstract class FilesystemAction : FuzzyAction() { minimumScore = priorityQueue.maybeAdd( minimumScore, fuzzyMatchContainer, - fileListLimit + fileListLimit, + globalState.prioritizeShorterDirPaths ) } } @@ -147,7 +149,7 @@ abstract class FilesystemAction : FuzzyAction() { val result = DefaultListModel() result.addAll( priorityQueue.sortedWith( - compareByDescending { it.getScore() }) + compareByDescending { it.getScore(prioritizeShorterDirPaths) }) ) return result } @@ -156,14 +158,15 @@ abstract class FilesystemAction : FuzzyAction() { minimumScore: Int?, fuzzyMatchContainer: FuzzyMatchContainer, fileListLimit: Int, + prioritizeShorterFilePaths: Boolean, ): Int? { var ret = minimumScore - if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + if (minimumScore == null || fuzzyMatchContainer.getScore(prioritizeShorterFilePaths) > minimumScore) { this.add(fuzzyMatchContainer) if (this.size > fileListLimit) { this.remove() - ret = this.peek().getScore() + ret = this.peek().getScore(prioritizeShorterFilePaths) } } @@ -196,6 +199,7 @@ abstract class FilesystemAction : FuzzyAction() { searchString, globalState.fileListLimit, projectState.ignoredCharacters, + globalState.prioritizeShorterDirPaths, ) } coroutineContext.ensureActive() diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt index ce4a84a8..c98798bc 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt @@ -164,17 +164,20 @@ class TestBenchComponent : JPanel(), Disposable { collectIterationFiles(project) } + val prioritizeShorterDirPaths = liveSettingsComponent.prioritizeShortDirs.getCheckBox().isSelected val listModel = withContext(Dispatchers.Default) { processIterationEntries( iterationEntries, stringEvaluator, searchString, liveSettingsComponent.fileListLimit.getIntSpinner().value as Int, + prioritizeShorterDirPaths, ) } val sortedList = - listModel.elements().toList().sortedByDescending { (it as FuzzyMatchContainer).getScore() } + listModel.elements().toList() + .sortedByDescending { (it as FuzzyMatchContainer).getScore(prioritizeShorterDirPaths) } val data: Array> = sortedList.map { arrayOf( (it as FuzzyMatchContainer).filename as Any, @@ -236,11 +239,13 @@ class TestBenchComponent : JPanel(), Disposable { 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() }) + fileListLimit + 1, + compareBy { it.getScore(prioritizeShorterDirPaths) }) val queueLock = Any() var minimumScore: Int? = null @@ -267,7 +272,7 @@ class TestBenchComponent : JPanel(), Disposable { container?.let { fuzzyMatchContainer -> synchronized(queueLock) { minimumScore = priorityQueue.maybeAdd( - minimumScore, fuzzyMatchContainer, fileListLimit + minimumScore, fuzzyMatchContainer, fileListLimit, prioritizeShorterDirPaths ) } } @@ -283,21 +288,24 @@ class TestBenchComponent : JPanel(), Disposable { val result = DefaultListModel() result.addAll( priorityQueue.sortedWith( - compareByDescending { it.getScore() }) + compareByDescending { it.getScore(prioritizeShorterDirPaths) }) ) return result } private fun PriorityQueue.maybeAdd( - minimumScore: Int?, fuzzyMatchContainer: FuzzyMatchContainer, fileListLimit: Int + minimumScore: Int?, + fuzzyMatchContainer: FuzzyMatchContainer, + fileListLimit: Int, + prioritizeShorterDirPaths: Boolean, ): Int? { var ret = minimumScore - if (minimumScore == null || fuzzyMatchContainer.getScore() > minimumScore) { + if (minimumScore == null || fuzzyMatchContainer.getScore(prioritizeShorterDirPaths) > minimumScore) { this.add(fuzzyMatchContainer) if (this.size > fileListLimit) { this.remove() - ret = this.peek().getScore() + ret = this.peek().getScore(prioritizeShorterDirPaths) } } 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 index 7358482f..e5dc366b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/IterationEntry.kt @@ -27,5 +27,6 @@ package com.mituuz.fuzzier.entities data class IterationEntry( val name: String, val path: String, - val module: String + val module: String, + val isDir: Boolean ) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 6c3ec09e..d63fc673 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -23,6 +23,8 @@ */ package com.mituuz.fuzzier.entities +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 @@ -41,11 +43,12 @@ class StringEvaluator( val moduleBasePath = modules[moduleName] ?: return null - val dirPath = iteratorEntry.path.removePrefix(moduleBasePath) - if (isExcluded(dirPath)) return null + val filePath = iteratorEntry.path.removePrefix(moduleBasePath) + if (isExcluded(filePath)) return null - if (dirPath.isNotBlank()) { - return createFuzzyContainer(dirPath, moduleBasePath, scoreCalculator) + val fileType = if (iteratorEntry.isDir) FileType.DIR else FileType.FILE + if (filePath.isNotBlank()) { + return createFuzzyContainer(filePath, moduleBasePath, scoreCalculator, fileType) } return null @@ -74,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 index 7b6d69a4..991cca57 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/intellij/iteration/IntelliJIterationFileCollector.kt @@ -39,7 +39,7 @@ class IntelliJIterationFileCollector : IterationFileCollector { if (!shouldContinue()) return@iterateContent false if (fileFilter(vf)) { - val iteratorEntry = IterationEntry(vf.name, vf.path, moduleName) + val iteratorEntry = IterationEntry(vf.name, vf.path, moduleName, vf.isDirectory) add(iteratorEntry) } diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt index 81de8382..1687d875 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/util/FuzzierUtil.kt @@ -28,19 +28,10 @@ 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.FuzzyContainer -import com.mituuz.fuzzier.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import java.awt.Rectangle -import java.util.* -import javax.swing.DefaultListModel class FuzzierUtil { - private var globalState = service().state - private var listLimit: Int = globalState.fileListLimit - private var prioritizeShorterDirPaths = globalState.prioritizeShorterDirPaths - companion object { /** * Create a dimension key for a specific screen bounds @@ -60,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/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt index 8841005b..aa879c8c 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyActionTest.kt @@ -35,6 +35,7 @@ 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) diff --git a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt index 013cf974..49cd2128 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/FuzzyMoverTest.kt @@ -189,7 +189,13 @@ class FuzzyMoverTest { val listModel = DefaultListModel() if (virtualFile != null) { val container = - FuzzyMatchContainer(FuzzyMatchContainer.FuzzyScore(), virtualFile.path, virtualFile.name, "") + 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 index 3f8126c1..c6786b3c 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/StringEvaluatorTest.kt @@ -45,7 +45,7 @@ class StringEvaluatorTest { 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) + val entry = IterationEntry(name = name, path = fullPath, module = moduleName, isDir = false) evaluator.evaluateIteratorEntry( entry, "", MatchConfig() 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/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 From 530b4a297af72f060095f0b7bc660c8f40732196 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 10:36:24 +0200 Subject: [PATCH 49/61] Cleanup --- src/asd.asd | 0 src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/asd.asd diff --git a/src/asd.asd b/src/asd.asd new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index ba308bd2..3a8fc9ec 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -130,7 +130,7 @@ open class Fuzzier : FilesystemAction() { component.fileList.setPaintBusy(true) ApplicationManager.getApplication().executeOnPooledThread { try { - val editorHistoryManager = EditorHistoryManager.Companion.getInstance(project) + val editorHistoryManager = EditorHistoryManager.getInstance(project) val listModel = when (globalState.recentFilesMode) { FuzzierGlobalSettingsService.RecentFilesMode.RECENT_PROJECT_FILES -> InitialViewHandler.Companion.getRecentProjectFiles( @@ -176,7 +176,7 @@ open class Fuzzier : FilesystemAction() { } } if (fuzzyContainer != null) { - InitialViewHandler.Companion.addFileToRecentlySearchedFiles(fuzzyContainer, projectState, globalState) + InitialViewHandler.addFileToRecentlySearchedFiles(fuzzyContainer, projectState, globalState) } popup.cancel() } From 3a41d64dec5650a775efd9d30f360d35a56fc56b Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 10:40:24 +0200 Subject: [PATCH 50/61] Add tests for activation bindings --- src/asd.asd | 0 .../ui/bindings/ActivationBindingsTest.kt | 89 +++++++++++++++++++ 2 files changed, 89 insertions(+) delete mode 100644 src/asd.asd create mode 100644 src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt diff --git a/src/asd.asd b/src/asd.asd deleted file mode 100644 index e69de29b..00000000 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..d75961a1 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt @@ -0,0 +1,89 @@ +/* + * 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 `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 From 0451f911dcd210c82bf1d84bd7040f9200b62731 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 10:41:26 +0200 Subject: [PATCH 51/61] Add case for single click --- .../ui/bindings/ActivationBindingsTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt index d75961a1..686c984d 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/ui/bindings/ActivationBindingsTest.kt @@ -44,6 +44,30 @@ class ActivationBindingsTest { 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 From 21c119351ed3fbd76c1798848788e9622e119c8f Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 11:05:09 +0200 Subject: [PATCH 52/61] Add test for popup provider --- .../fuzzier/ui/popup/AutoSizePopupProvider.kt | 3 +- .../fuzzier/ui/popup/DefaultPopupProvider.kt | 29 +++--- .../ui/popup/DefaultPopupProviderTest.kt | 97 +++++++++++++++++++ 3 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt index c2174e8f..1c8915d6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt @@ -44,9 +44,8 @@ class AutoSizePopupProvider : PopupProvider { config: PopupConfig, cleanupFunction: () -> Unit, ): JBPopup { - // TODO: Check the error handling here val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component - ?: error("No IDE frame found for project") + ?: error("No IDE frame found for project, cannot setup popup") val windowWidth = (mainWindow.width * 0.8).toInt() val windowHeight = (mainWindow.height * 0.8).toInt() diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt index a71c19cf..d7b11bb4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt @@ -35,7 +35,11 @@ import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey import java.awt.Component import javax.swing.JComponent -class DefaultPopupProvider : PopupProvider { +class DefaultPopupProvider( + private val windowManager: WindowManager = WindowManager.getInstance(), + private val popupFactory: JBPopupFactory = JBPopupFactory.getInstance(), + private val dimensionService: DimensionService = DimensionService.getInstance(), +) : PopupProvider { override fun show( project: Project, content: JComponent, @@ -43,20 +47,19 @@ class DefaultPopupProvider : PopupProvider { config: PopupConfig, cleanupFunction: () -> Unit, ): JBPopup { - // TODO: Check the error handling here - val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component - ?: error("No IDE frame found for project") + val mainWindow: Component = windowManager.getIdeFrame(project)?.component + ?: error("No IDE frame found for project, cannot setup popup") val screenBounds = mainWindow.graphicsConfiguration.bounds val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds) if (config.resetWindow()) { - DimensionService.getInstance().setSize(screenDimensionKey, config.preferredSizeProvider, null) - DimensionService.getInstance().setLocation(screenDimensionKey, null, null) + dimensionService.setSize(screenDimensionKey, config.preferredSizeProvider, null) + dimensionService.setLocation(screenDimensionKey, null, null) config.clearResetWindowFlag() } - val popup = JBPopupFactory.getInstance() + val popup = popupFactory .createComponentPopupBuilder(content, focus) .setFocusable(true) .setRequestFocus(true) @@ -69,12 +72,14 @@ class DefaultPopupProvider : PopupProvider { popup.showInCenterOf(mainWindow) - popup.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - cleanupFunction() - } - }) + popup.addListener(createCleanupListener(cleanupFunction)) return popup } + + internal fun createCleanupListener(cleanupFunction: () -> Unit): JBPopupListener = object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + cleanupFunction() + } + } } \ No newline at end of file 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..41150c5c --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.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.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.* +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 throws 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 ex = assertThrows(IllegalStateException::class.java) { + provider.show(project, content, focus, cfg) { /* cleanup */ } + } + assertTrue(ex.message!!.startsWith("No IDE frame found for project")) + + // Since we fail 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 From 058cf7961182655a6cd4b57f77a3aa83dd607ff8 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 11:26:40 +0200 Subject: [PATCH 53/61] Handle possibility of a null popup --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 5 ++++- src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 7 ++++++- src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt | 5 ++++- .../fuzzier/ui/popup/AutoSizePopupProvider.kt | 4 ++-- .../fuzzier/ui/popup/DefaultPopupProvider.kt | 4 ++-- .../fuzzier/ui/popup/DefaultPopupProviderTest.kt | 15 +++++++-------- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 3a8fc9ec..d29e7ee1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -58,7 +58,7 @@ open class Fuzzier : FilesystemAction() { component = FuzzyFinderComponent(project) previewAlarm = getPreviewAlarm() createListeners(project) - popup = popupProvider.show( + val maybePopup = popupProvider.show( project = project, content = component, focus = component.searchField, @@ -72,6 +72,9 @@ open class Fuzzier : FilesystemAction() { cleanupFunction = { cleanupPopup() } ) + if (maybePopup == null) return@invokeLater + popup = maybePopup + createSharedListeners(project) (component as FuzzyFinderComponent).splitPane.dividerLocation = diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 12daefe3..233c6f20 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -42,6 +42,8 @@ import com.intellij.openapi.project.Project 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 @@ -126,7 +128,7 @@ open class FuzzyGrep : FuzzyAction() { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project, showSecondaryField = useRg) createListeners(project) - popup = popupProvider.show( + val maybePopup = popupProvider.show( project = project, content = component, focus = component.searchField, @@ -140,6 +142,9 @@ open class FuzzyGrep : FuzzyAction() { cleanupFunction = { cleanupPopup() }, ) + if (maybePopup == null) return@launch + popup = maybePopup + createSharedListeners(project) (component as FuzzyFinderComponent).splitPane.dividerLocation = diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index f588a200..dc31d153 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -63,7 +63,7 @@ class FuzzyMover : FilesystemAction() { component.fileList.setEmptyText("Press enter to use current file: ${currentFile.path}") } - popup = popupProvider.show( + val maybePopup = popupProvider.show( project = project, content = component, focus = component.searchField, @@ -76,6 +76,9 @@ class FuzzyMover : FilesystemAction() { cleanupFunction = { cleanupPopup() }, ) + if (maybePopup == null) return@invokeLater + popup = maybePopup + createSharedListeners(project) } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt index 1c8915d6..1aa7dd90 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt @@ -43,9 +43,9 @@ class AutoSizePopupProvider : PopupProvider { focus: JComponent, config: PopupConfig, cleanupFunction: () -> Unit, - ): JBPopup { + ): JBPopup? { val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component - ?: error("No IDE frame found for project, cannot setup popup") + ?: return null val windowWidth = (mainWindow.width * 0.8).toInt() val windowHeight = (mainWindow.height * 0.8).toInt() diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt index d7b11bb4..6e3379ce 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt @@ -46,9 +46,9 @@ class DefaultPopupProvider( focus: JComponent, config: PopupConfig, cleanupFunction: () -> Unit, - ): JBPopup { + ): JBPopup? { val mainWindow: Component = windowManager.getIdeFrame(project)?.component - ?: error("No IDE frame found for project, cannot setup popup") + ?: return null val screenBounds = mainWindow.graphicsConfiguration.bounds val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds) diff --git a/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt index 41150c5c..e3b042f4 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProviderTest.kt @@ -30,7 +30,8 @@ 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.* +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 @@ -51,7 +52,7 @@ class DefaultPopupProviderTest { } @Test - fun `show throws when no IDE frame is available`() { + 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 @@ -70,12 +71,10 @@ class DefaultPopupProviderTest { ) try { - val ex = assertThrows(IllegalStateException::class.java) { - provider.show(project, content, focus, cfg) { /* cleanup */ } - } - assertTrue(ex.message!!.startsWith("No IDE frame found for project")) - - // Since we fail early due to missing IDE frame, ensure no side-effects were triggered + 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() From 9799d4cab1dd8e053eba352baccd853c8fab0e57 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 11:37:49 +0200 Subject: [PATCH 54/61] Add popup sizing selector --- .../FuzzierGlobalSettingsComponent.kt | 33 +++++++++++- .../fuzzier/components/SettingsComponent.kt | 52 +++++++++++-------- .../FuzzierGlobalSettingsConfigurable.kt | 18 +++++++ .../settings/FuzzierGlobalSettingsService.kt | 7 +++ 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt index 18765f76..817f5f1f 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,16 @@ 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 dimensionComponent = JPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) add(JBLabel("Width: ")) @@ -309,6 +318,7 @@ class FuzzierGlobalSettingsComponent( .addComponent(filenameTypeSelector) .addComponent(highlightFilename) .addComponent(searchPosition) + .addComponent(popupSizingSelector) .addComponent(defaultDimension) .addComponent(previewFontSize) .addComponent(fileListUseEditorFont) @@ -383,6 +393,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/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/settings/FuzzierGlobalSettingsConfigurable.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt index 8d11bdb3..c8c5da7e 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,6 +48,7 @@ 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.searchPosition.getSearchPositionComboBox().selectedIndex = state.searchPosition.ordinal @@ -65,6 +67,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 +84,12 @@ class FuzzierGlobalSettingsConfigurable : Configurable { return component.jPanel } + private fun updateDimensionVisibility(sizing: PopupSizing) { + val visible = sizing != PopupSizing.AUTO_SIZE + component.defaultDimension.label.isVisible = visible + component.defaultDimension.component.isVisible = visible + } + override fun isModified(): Boolean { val newGlobalSet = component.globalExclusionTextArea.text .lines() @@ -84,6 +99,7 @@ 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.searchPosition != component.searchPosition.getSearchPositionComboBox().selectedItem @@ -112,6 +128,8 @@ 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 diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt index e76e410c..afaae006 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt @@ -48,6 +48,8 @@ class FuzzierGlobalSettingsService : PersistentStateComponent Date: Sun, 14 Dec 2025 11:44:38 +0200 Subject: [PATCH 55/61] Add tests for new selector --- .../FuzzierGlobalSettingsConfigurableTest.kt | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) 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() From eea6750f2e4819c0d63e4907180f7e65ad335b36 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 12:00:14 +0200 Subject: [PATCH 56/61] Handle popup provider selection based on settings --- src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt | 4 +- .../kotlin/com/mituuz/fuzzier/FuzzyGrep.kt | 4 +- .../kotlin/com/mituuz/fuzzier/FuzzyMover.kt | 4 +- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 10 +++ .../FuzzyActionProviderSelectionTest.kt | 73 +++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 src/test/kotlin/com/mituuz/fuzzier/actions/FuzzyActionProviderSelectionTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index d29e7ee1..4253464d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -38,7 +38,6 @@ import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.ui.bindings.ActivationBindings -import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider import com.mituuz.fuzzier.ui.popup.PopupConfig import com.mituuz.fuzzier.util.InitialViewHandler import javax.swing.DefaultListModel @@ -46,7 +45,6 @@ import javax.swing.DefaultListModel open class Fuzzier : FilesystemAction() { private var previewAlarm: SingleAlarm? = null private var lastPreviewKey: String? = null - private val popupProvider = DefaultPopupProvider() protected open var popupTitle = "Fuzzy Search" override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean = @@ -58,7 +56,7 @@ open class Fuzzier : FilesystemAction() { component = FuzzyFinderComponent(project) previewAlarm = getPreviewAlarm() createListeners(project) - val maybePopup = popupProvider.show( + val maybePopup = getPopupProvider().show( project = project, content = component, focus = component.searchField, diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt index 233c6f20..1ba9c737 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyGrep.kt @@ -49,7 +49,6 @@ 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.DefaultPopupProvider import com.mituuz.fuzzier.ui.popup.PopupConfig import kotlinx.coroutines.* import org.apache.commons.lang3.StringUtils @@ -70,7 +69,6 @@ open class FuzzyGrep : FuzzyAction() { var useRg = true val isWindows = System.getProperty("os.name").lowercase().contains("win") private var currentLaunchJob: Job? = null - private val popupProvider = DefaultPopupProvider() protected open lateinit var popupTitle: String override fun runAction( @@ -128,7 +126,7 @@ open class FuzzyGrep : FuzzyAction() { defaultDoc = EditorFactory.getInstance().createDocument("") component = FuzzyFinderComponent(project, showSecondaryField = useRg) createListeners(project) - val maybePopup = popupProvider.show( + val maybePopup = getPopupProvider().show( project = project, content = component, focus = component.searchField, diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index dc31d153..8dfdf0c9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -40,14 +40,12 @@ import com.intellij.psi.PsiManager import com.mituuz.fuzzier.actions.filesystem.FilesystemAction import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.ui.bindings.ActivationBindings -import com.mituuz.fuzzier.ui.popup.DefaultPopupProvider import com.mituuz.fuzzier.ui.popup.PopupConfig import javax.swing.DefaultListModel class FuzzyMover : FilesystemAction() { lateinit var movableFile: PsiFile lateinit var currentFile: VirtualFile - private val popupProvider = DefaultPopupProvider() override fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { return { vf -> if (component.isDirSelector) vf.isDirectory else !vf.isDirectory } @@ -63,7 +61,7 @@ class FuzzyMover : FilesystemAction() { component.fileList.setEmptyText("Press enter to use current file: ${currentFile.path}") } - val maybePopup = popupProvider.show( + val maybePopup = getPopupProvider().show( project = project, content = component, focus = component.searchField, diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 75c7147e..87402c84 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -45,6 +45,9 @@ 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 kotlinx.coroutines.* import java.awt.Component @@ -71,6 +74,13 @@ abstract class FuzzyAction : AnAction() { protected open var currentUpdateListContentJob: Job? = null protected open var actionScope: CoroutineScope? = null + protected fun getPopupProvider(): PopupProvider { + return when (globalState.popupSizing) { + FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE -> AutoSizePopupProvider() + FuzzierGlobalSettingsService.PopupSizing.VANILLA -> DefaultPopupProvider() + } + } + override fun actionPerformed(actionEvent: AnActionEvent) { val project = actionEvent.project if (project != null) { 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) + } +} From e5b2378dd4b1f2c167eafd2fd168d96250e95921 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 12:43:08 +0200 Subject: [PATCH 57/61] Share some code and add a option to change auto size percentage --- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 7 +- .../FuzzierGlobalSettingsComponent.kt | 18 +++ .../FuzzierGlobalSettingsConfigurable.kt | 17 ++- .../settings/FuzzierGlobalSettingsService.kt | 4 + .../fuzzier/ui/popup/AutoSizePopupProvider.kt | 48 +++----- .../fuzzier/ui/popup/DefaultPopupProvider.kt | 25 +---- .../fuzzier/ui/popup/PopupProviderBase.kt | 58 ++++++++++ .../ui/popup/AutoSizePopupProviderTest.kt | 105 ++++++++++++++++++ 8 files changed, 228 insertions(+), 54 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/ui/popup/PopupProviderBase.kt create mode 100644 src/test/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProviderTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 87402c84..482da7ad 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -76,7 +76,12 @@ abstract class FuzzyAction : AnAction() { protected fun getPopupProvider(): PopupProvider { return when (globalState.popupSizing) { - FuzzierGlobalSettingsService.PopupSizing.AUTO_SIZE -> AutoSizePopupProvider() + 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() } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt index 817f5f1f..a9376ca1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt @@ -175,6 +175,23 @@ class FuzzierGlobalSettingsComponent( 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: ")) @@ -319,6 +336,7 @@ class FuzzierGlobalSettingsComponent( .addComponent(highlightFilename) .addComponent(searchPosition) .addComponent(popupSizingSelector) + .addComponent(autoSizePercentages) .addComponent(defaultDimension) .addComponent(previewFontSize) .addComponent(fileListUseEditorFont) diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt index c8c5da7e..8809cb38 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt @@ -51,6 +51,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable { 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 @@ -85,9 +87,12 @@ class FuzzierGlobalSettingsConfigurable : Configurable { } private fun updateDimensionVisibility(sizing: PopupSizing) { - val visible = sizing != PopupSizing.AUTO_SIZE - component.defaultDimension.label.isVisible = visible - component.defaultDimension.component.isVisible = visible + 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 { @@ -102,6 +107,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable { || 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 @@ -133,6 +140,8 @@ class FuzzierGlobalSettingsConfigurable : Configurable { 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 || @@ -146,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 afaae006..fa63b0e5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt @@ -50,6 +50,10 @@ class FuzzierGlobalSettingsService : PersistentStateComponent Unit, ): JBPopup? { - val mainWindow: Component = WindowManager.getInstance().getIdeFrame(project)?.component + val mainWindow: Component = getMainWindow(project) ?: return null - val windowWidth = (mainWindow.width * 0.8).toInt() - val windowHeight = (mainWindow.height * 0.8).toInt() - val popupSize = Dimension(windowWidth, windowHeight) - - val screenBounds = mainWindow.graphicsConfiguration.bounds - val screenDimensionKey = createDimensionKey(config.dimensionKey, screenBounds) - - if (config.resetWindow()) { - DimensionService.getInstance().setSize(screenDimensionKey, config.preferredSizeProvider, null) - DimensionService.getInstance().setLocation(screenDimensionKey, null, null) - config.clearResetWindowFlag() - } + val popupSize = computePopupSize(mainWindow) - val popup = JBPopupFactory.getInstance() - .createComponentPopupBuilder(content, focus) - .setFocusable(true) - .setRequestFocus(true) - .setResizable(true) - .setTitle(config.title) - .setMovable(true) - .setShowBorder(true) + val popup = baseBuilder(content, focus, config.title) .createPopup() popup.size = popupSize popup.showInCenterOf(mainWindow) - popup.addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - cleanupFunction() - } - }) + 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 index 6e3379ce..d4aa12f3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/DefaultPopupProvider.kt @@ -27,8 +27,6 @@ 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.ui.popup.JBPopupListener -import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.DimensionService import com.intellij.openapi.wm.WindowManager import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey @@ -36,10 +34,10 @@ import java.awt.Component import javax.swing.JComponent class DefaultPopupProvider( - private val windowManager: WindowManager = WindowManager.getInstance(), - private val popupFactory: JBPopupFactory = JBPopupFactory.getInstance(), + windowManager: WindowManager = WindowManager.getInstance(), + popupFactory: JBPopupFactory = JBPopupFactory.getInstance(), private val dimensionService: DimensionService = DimensionService.getInstance(), -) : PopupProvider { +) : PopupProviderBase(windowManager, popupFactory) { override fun show( project: Project, content: JComponent, @@ -47,7 +45,7 @@ class DefaultPopupProvider( config: PopupConfig, cleanupFunction: () -> Unit, ): JBPopup? { - val mainWindow: Component = windowManager.getIdeFrame(project)?.component + val mainWindow: Component = getMainWindow(project) ?: return null val screenBounds = mainWindow.graphicsConfiguration.bounds @@ -59,15 +57,8 @@ class DefaultPopupProvider( config.clearResetWindowFlag() } - val popup = popupFactory - .createComponentPopupBuilder(content, focus) - .setFocusable(true) - .setRequestFocus(true) - .setResizable(true) + val popup = baseBuilder(content, focus, config.title) .setDimensionServiceKey(null, screenDimensionKey, true) - .setTitle(config.title) - .setMovable(true) - .setShowBorder(true) .createPopup() popup.showInCenterOf(mainWindow) @@ -76,10 +67,4 @@ class DefaultPopupProvider( return popup } - - internal fun createCleanupListener(cleanupFunction: () -> Unit): JBPopupListener = object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - cleanupFunction() - } - } } \ 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/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) + } +} From e0c0218d014db392b4ef5aa1166403c3858e7fa8 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 12:43:46 +0200 Subject: [PATCH 58/61] Remove unused param --- .../kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt index f64d2d58..d9177cb1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/popup/AutoSizePopupProvider.kt @@ -27,7 +27,6 @@ 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 java.awt.Component import java.awt.Dimension @@ -38,7 +37,6 @@ class AutoSizePopupProvider( private val heightFactor: Double, windowManager: WindowManager = WindowManager.getInstance(), popupFactory: JBPopupFactory = JBPopupFactory.getInstance(), - @Suppress("unused") private val dimensionService: DimensionService = DimensionService.getInstance(), ) : PopupProviderBase(windowManager, popupFactory) { override fun show( From 3fcc5c935744eb0e8bac691a89990b7b829e7a62 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 12:50:37 +0200 Subject: [PATCH 59/61] Update changelog --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1422bf6c..166cd6d0 100644 --- a/changelog.md +++ b/changelog.md @@ -2,10 +2,11 @@ ## Version 1.15.0 -- Refactor fuzzy file search to use coroutines +- 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 From 84b806e1cdccdc1cb31c4fb5eeb2e9366069d710 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 14 Dec 2025 12:56:18 +0200 Subject: [PATCH 60/61] Clear up settings --- .../fuzzier/settings/FuzzierGlobalSettingsService.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt index fa63b0e5..88887a18 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt @@ -45,14 +45,13 @@ class FuzzierGlobalSettingsService : PersistentStateComponent Date: Sun, 14 Dec 2025 12:58:40 +0200 Subject: [PATCH 61/61] Update changelog --- build.gradle.kts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index cc7c4bfb..b3fbcaed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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()