diff --git a/build.gradle.kts b/build.gradle.kts index b0a376dc..14bd09a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } // Use same version and group for the jar and the plugin -val currentVersion = "1.2.0" +val currentVersion = "1.3.0" val myGroup = "com.mituuz" version = currentVersion group = myGroup @@ -33,10 +33,10 @@ dependencies { testFramework(TestFrameworkType.Platform) } - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") - testImplementation("org.mockito:mockito-core:5.12.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.3") + testImplementation("org.mockito:mockito-core:5.14.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.3") // Required to fix issue where JUnit5 Test Framework refers to JUnit4 // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4 @@ -63,10 +63,8 @@ intellijPlatform { changeNotes = """

Version $currentVersion

- - Make popup dimensions persistent across projects
-  - Popup dimensions are saved per screen bounds (location and size)
- - Improve popup location consistency (fixes right screen, left half issue)
- - Update kotlin-jvm and intellij-platform plugins to 2.1.0 + - Add option to list recently searched files on popup open
+ - Update some dependencies """.trimIndent() ideaVersion { diff --git a/changelog.md b/changelog.md index d27b8618..431fd28b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,8 @@ # Changelog +## Version 1.3.0 +- Add option to list recently searched files on popup open +- Update some dependencies + ## Version 1.2.0 - Make popup dimensions persistent across projects - Improve popup location consistency (fixes right screen, left half issue) diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 143edd1d..d615ed39 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -47,9 +47,13 @@ import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.wm.WindowManager import com.mituuz.fuzzier.components.FuzzyFinderComponent import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.NONE +import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.RECENTLY_SEARCHED_FILES +import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.RECENT_PROJECT_FILES import com.mituuz.fuzzier.util.FuzzierUtil import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey +import com.mituuz.fuzzier.util.InitialViewHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -66,6 +70,7 @@ open class Fuzzier : FuzzyAction() { private var defaultDoc: Document? = null open var title: String = "Fuzzy Search" private val fuzzyDimensionKey: String = "FuzzySearchPopup" + // Used by FuzzierVCS to check if files are tracked by the VCS protected var changeListManager: ChangeListManager? = null @@ -133,24 +138,19 @@ open class Fuzzier : FuzzyAction() { */ private fun createInitialView(project: Project) { ApplicationManager.getApplication().executeOnPooledThread { - val editorHistory = EditorHistoryManager.getInstance(project).fileList - val listModel = DefaultListModel() - val limit = fuzzierSettingsService.state.fileListLimit - - // Start from the end of editor history (most recent file) - var i = editorHistory.size - 1 - while (i >= 0 && listModel.size() < limit) { - val file = editorHistory[i] - val filePathAndModule = fuzzierUtil.removeModulePath(file.path) - // Don't add files that do not have a module path in the project - if (filePathAndModule.second == "") { - i-- - continue + val editorHistoryManager = EditorHistoryManager.getInstance(project) + + val listModel = when (fuzzierSettingsService.state.recentFilesMode) { + RECENT_PROJECT_FILES -> InitialViewHandler.getRecentProjectFiles( + fuzzierSettingsService, + fuzzierUtil, + editorHistoryManager + ) + + RECENTLY_SEARCHED_FILES -> InitialViewHandler.getRecentlySearchedFiles(fuzzierSettingsService) + else -> { + DefaultListModel() } - val fuzzyMatchContainer = - FuzzyMatchContainer.createOrderedContainer(i, filePathAndModule.first, filePathAndModule.second, file.name) - listModel.addElement(fuzzyMatchContainer) - i-- } ApplicationManager.getApplication().invokeLater { @@ -205,7 +205,7 @@ open class Fuzzier : FuzzyAction() { } } } - + private fun handleEmptySearchString(project: Project) { if (fuzzierSettingsService.state.recentFilesMode != NONE) { createInitialView(project) @@ -216,7 +216,7 @@ open class Fuzzier : FuzzyAction() { } } } - + private fun getStringEvaluator(): StringEvaluator { return StringEvaluator( fuzzierSettingsService.state.exclusionSet, @@ -224,9 +224,11 @@ open class Fuzzier : FuzzyAction() { changeListManager ) } - - private fun process(project: Project, stringEvaluator: StringEvaluator, searchString: String, - listModel: DefaultListModel, task: Future<*>?) { + + private fun process( + project: Project, stringEvaluator: StringEvaluator, searchString: String, + listModel: DefaultListModel, task: Future<*>? + ) { val moduleManager = ModuleManager.getInstance(project) if (fuzzierSettingsService.state.isProject) { processProject(project, stringEvaluator, searchString, listModel, task) @@ -234,16 +236,20 @@ open class Fuzzier : FuzzyAction() { processModules(moduleManager, stringEvaluator, searchString, listModel, task) } } - - private fun processProject(project: Project, stringEvaluator: StringEvaluator, - searchString: String, listModel: DefaultListModel, task: Future<*>?) { + + 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<*>?) { + 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) @@ -271,7 +277,7 @@ open class Fuzzier : FuzzyAction() { } } - private fun openFile(project: Project, virtualFile: VirtualFile) { + private fun openFile(project: Project, fuzzyMatchContainer: FuzzyMatchContainer?, virtualFile: VirtualFile) { val fileEditorManager = FileEditorManager.getInstance(project) val currentEditor = fileEditorManager.selectedTextEditor val previousFile = currentEditor?.virtualFile @@ -288,6 +294,9 @@ open class Fuzzier : FuzzyAction() { } } } + if (fuzzyMatchContainer != null) { + InitialViewHandler.addFileToRecentlySearchedFiles(fuzzyMatchContainer, fuzzierSettingsService) + } popup?.cancel() } @@ -324,7 +333,7 @@ open class Fuzzier : FuzzyAction() { VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") // Open the file in the editor virtualFile?.let { - openFile(project, it) + openFile(project, selectedValue, it) } } } @@ -341,7 +350,7 @@ open class Fuzzier : FuzzyAction() { val virtualFile = VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") virtualFile?.let { - openFile(project, it) + openFile(project, selectedValue, it) } } }) diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt index 27dd4a2f..76c4ab0d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyMover.kt @@ -48,6 +48,7 @@ import com.intellij.psi.PsiManager import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil import com.mituuz.fuzzier.components.SimpleFinderComponent import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.util.FuzzierUtil.Companion.createDimensionKey import org.apache.commons.lang3.StringUtils import java.awt.Point diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt index bc8b4d26..864c47a6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt @@ -38,7 +38,7 @@ import com.intellij.ui.components.JBTextArea import com.intellij.ui.table.JBTable import com.intellij.uiDesigner.core.GridConstraints import com.intellij.uiDesigner.core.GridLayoutManager -import com.mituuz.fuzzier.StringEvaluator +import com.mituuz.fuzzier.entities.StringEvaluator import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt index e52f6c7e..5c65887e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt @@ -24,15 +24,35 @@ SOFTWARE. package com.mituuz.fuzzier.entities import com.intellij.openapi.components.service +import com.intellij.util.xmlb.Converter +import com.intellij.util.xmlb.XmlSerializationException import com.mituuz.fuzzier.settings.FuzzierConfiguration.END_STYLE_TAG import com.mituuz.fuzzier.settings.FuzzierConfiguration.startStyleTag import com.mituuz.fuzzier.settings.FuzzierSettingsService - -class FuzzyMatchContainer(val score: FuzzyScore, var filePath: String, var filename: String, private var module: String = "") { +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 javax.swing.DefaultListModel + +class FuzzyMatchContainer( + val score: FuzzyScore, + var filePath: String, + var filename: String, + private var module: String = "" +) : Serializable { + @Transient private var initialPath: String? = null companion object { - fun createOrderedContainer(order: Int, filePath: String, initialPath:String, filename: String): FuzzyMatchContainer { + fun createOrderedContainer( + order: Int, + filePath: String, + initialPath: String, + filename: String + ): FuzzyMatchContainer { val fuzzyScore = FuzzyScore() fuzzyScore.filenameScore = order val fuzzyMatchContainer = FuzzyMatchContainer(fuzzyScore, filePath, filename) @@ -105,7 +125,7 @@ class FuzzyMatchContainer(val score: FuzzyScore, var filePath: String, var filen FILENAME_WITH_PATH_STYLED("Filename with (path) styled") } - class FuzzyScore { + class FuzzyScore : Serializable { var streakScore = 0 var multiMatchScore = 0 var partialPathScore = 0 @@ -120,4 +140,37 @@ class FuzzyMatchContainer(val score: FuzzyScore, var filePath: String, var filen override fun toString(): String { return "FuzzyMatchContainer: $filename, score: ${getScore()}, dir score: ${getScoreWithDirLength()}" } + + /** + * This is necessary to persists recently used files between IDE restarts + * + * Uses a base 64 encoded string + * + * ``` + * @OptionTag(converter = FuzzyMatchContainer.FuzzyMatchContainerConverter::class) + * var recentlySearchedFiles: DefaultListModel? = DefaultListModel() + * ``` + * + * @see FuzzierSettingsService + */ + class FuzzyMatchContainerConverter : Converter>() { + override fun fromString(value: String) : DefaultListModel { + // Fallback to an empty list if deserialization fails + try { + val data = Base64.getDecoder().decode(value) + val byteArrayInputStream = ByteArrayInputStream(data) + return ObjectInputStream(byteArrayInputStream).use { it.readObject() as DefaultListModel } + } catch (_: XmlSerializationException) { + return DefaultListModel(); + } catch (_: IllegalArgumentException) { + return DefaultListModel(); + } + } + + override fun toString(value: DefaultListModel) : String { + val byteArrayOutputStream = ByteArrayOutputStream() + ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(value) } + return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/StringEvaluator.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt similarity index 97% rename from src/main/kotlin/com/mituuz/fuzzier/StringEvaluator.kt rename to src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt index 26d761fd..b107e1c5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/StringEvaluator.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/StringEvaluator.kt @@ -21,13 +21,11 @@ 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 +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.entities.FuzzyMatchContainer -import com.mituuz.fuzzier.entities.ScoreCalculator import com.mituuz.fuzzier.util.FuzzierUtil import java.util.concurrent.Future import javax.swing.DefaultListModel @@ -92,7 +90,7 @@ class StringEvaluator( true } } - + fun evaluateFile(iterationFile: FuzzierUtil.IterationFile, listModel: DefaultListModel, searchString: String) { val scoreCalculator = ScoreCalculator(searchString) diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsService.kt index d6e383e3..35808061 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsService.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsService.kt @@ -26,9 +26,12 @@ package com.mituuz.fuzzier.settings import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.annotations.OptionTag +import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FilenameType import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FilenameType.FILE_PATH_ONLY import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.RECENT_PROJECT_FILES +import javax.swing.DefaultListModel @State( name = "com.mituuz.fuzzier.FuzzierSettings", @@ -39,6 +42,8 @@ class FuzzierSettingsService : PersistentStateComponent = HashMap() var isProject = false var recentFilesMode: RecentFilesMode = RECENT_PROJECT_FILES + @OptionTag(converter = FuzzyMatchContainer.FuzzyMatchContainerConverter::class) + var recentlySearchedFiles: DefaultListModel? = DefaultListModel() var splitPosition: Int = 300 var exclusionSet: Set = setOf("/.idea/*", "/.git/*", "/target/*", "/build/*", "/.gradle/*", "/.run/*") @@ -74,6 +79,7 @@ class FuzzierSettingsService : PersistentStateComponent { + /** + * 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 + */ + fun extractModulePath(filePath: String): Pair { val modules = settingsState.modules for (modulePath in modules.values) { if (filePath.contains(modulePath)) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/util/InitialViewHandler.kt b/src/main/kotlin/com/mituuz/fuzzier/util/InitialViewHandler.kt new file mode 100644 index 00000000..00bf6d75 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/util/InitialViewHandler.kt @@ -0,0 +1,115 @@ +/* +MIT License + +Copyright (c) 2024 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.fileEditor.impl.EditorHistoryManager +import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.settings.FuzzierSettingsService +import javax.swing.DefaultListModel + +class InitialViewHandler { + companion object { + fun getRecentProjectFiles( + fuzzierSettingsService: FuzzierSettingsService, fuzzierUtil: FuzzierUtil, + editorHistoryManager: EditorHistoryManager + ): DefaultListModel { + val editorHistory = editorHistoryManager.fileList + val listModel = DefaultListModel() + val limit = fuzzierSettingsService.state.fileListLimit + + // Start from the end of editor history (most recent file) + var i = editorHistory.size - 1 + while (i >= 0 && listModel.size() < limit) { + val file = editorHistory[i] + val filePathAndModule = fuzzierUtil.extractModulePath(file.path) + // Don't add files that do not have a module path in the project + if (filePathAndModule.second == "") { + i-- + continue + } + val fuzzyMatchContainer = FuzzyMatchContainer.createOrderedContainer( + i, filePathAndModule.first, filePathAndModule.second, file.name + ) + listModel.addElement(fuzzyMatchContainer) + i-- + } + + return listModel + } + + fun getRecentlySearchedFiles(fuzzierSettingsService: FuzzierSettingsService): DefaultListModel { + var listModel = fuzzierSettingsService.state.recentlySearchedFiles + + if (listModel == null) { + listModel = DefaultListModel() + fuzzierSettingsService.state.recentlySearchedFiles = listModel + } + + var i = 0 + while (i < listModel.size) { + if (listModel[i] == null) { + listModel.remove(i) + } else { + i++ + } + } + + // Reverse the list to show the most recent searches first + var result = DefaultListModel() + + var j = 0 + while (j < listModel.size) { + val index = listModel.size - j - 1 + result.addElement(listModel[index]) + j++ + } + + return result + } + + fun addFileToRecentlySearchedFiles(fuzzyMatchContainer: FuzzyMatchContainer, fuzzierSettingsService: FuzzierSettingsService) { + var listModel: DefaultListModel? = fuzzierSettingsService.state.recentlySearchedFiles + + if (listModel == null) { + listModel = DefaultListModel() + fuzzierSettingsService.state.recentlySearchedFiles = listModel + } + + var i = 0 + while (i < listModel.size) { + if (listModel[i].filePath == fuzzyMatchContainer.filePath) { + listModel.remove(i) + } else { + i++ + } + } + + while (listModel.size > fuzzierSettingsService.state.fileListLimit - 1) { + listModel.remove(listModel.size - 1) + } + + listModel.addElement(fuzzyMatchContainer) + } + } +} \ 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 d3b77626..b6046a17 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/TestUtil.kt @@ -38,6 +38,7 @@ import com.intellij.testFramework.fixtures.IdeaProjectTestFixture import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory import com.intellij.testFramework.runInEdtAndWait import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.StringEvaluator import org.mockito.ArgumentMatchers.any import javax.swing.DefaultListModel import org.mockito.Mockito diff --git a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt index efb3ef18..c4b22239 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainerTest.kt @@ -29,6 +29,11 @@ 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.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import javax.swing.DefaultListModel class FuzzyMatchContainerTest { @Suppress("unused") @@ -75,4 +80,40 @@ class FuzzyMatchContainerTest { i++ } } + + @Test + fun `Test serialization`() { + val score = FuzzyScore() + val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt") + val byteArrayOutputStream = ByteArrayOutputStream() + ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(container) } + + val byteArrayInputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray()) + val deserialized = ObjectInputStream(byteArrayInputStream).use { it.readObject() as FuzzyMatchContainer } + assertEquals("", deserialized.filePath) + assertEquals("FuzzyMatchContainerTest.kt", deserialized.filename) + } + + @Test + fun `Test default list serialization`() { + val list = DefaultListModel() + val score = FuzzyScore() + val container = FuzzyMatchContainer(score, "", "FuzzyMatchContainerTest.kt") + list.addElement(container) + + val converter = FuzzyMatchContainer.FuzzyMatchContainerConverter() + val stringRep = converter.toString(list) + + val deserialized: DefaultListModel = converter.fromString(stringRep) + assertEquals(1, deserialized.size) + assertEquals("", deserialized.get(0).filePath) + assertEquals("FuzzyMatchContainerTest.kt", deserialized.get(0).filename) + } + + @Test + fun `Deserialization fails`() { + val converter = FuzzyMatchContainer.FuzzyMatchContainerConverter() + 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/settings/FuzzierSettingsConfigurableTest.kt b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsConfigurableTest.kt index 4b45f3da..f4e53354 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsConfigurableTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/settings/FuzzierSettingsConfigurableTest.kt @@ -25,24 +25,29 @@ package com.mituuz.fuzzier.settings import com.intellij.openapi.components.service import com.intellij.testFramework.TestApplicationManager +import com.mituuz.fuzzier.entities.FuzzyMatchContainer import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FilenameType.FILENAME_WITH_PATH_STYLED import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.NONE import com.mituuz.fuzzier.settings.FuzzierSettingsService.RecentFilesMode.RECENT_PROJECT_FILES import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class FuzzierSettingsConfigurableTest { @Suppress("unused") private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance() private val state = service().state + private val settingsConfigurable = FuzzierSettingsConfigurable() - @Test - fun `Test is modified with no changes`() { + @BeforeEach + fun setUp() { state.exclusionSet = setOf("Hello", "There") + state.newTab = true - state.recentFilesMode = NONE + state.recentFilesMode = RECENT_PROJECT_FILES state.prioritizeShorterDirPaths = false state.debouncePeriod = 140 + state.resetWindow = false state.fileListLimit = 200 state.filenameType = FILENAME_WITH_PATH_STYLED @@ -57,36 +62,135 @@ class FuzzierSettingsConfigurableTest { state.matchWeightSingleChar = 6 state.matchWeightStreakModifier = 20 state.matchWeightFilename = 15 + } - val settingsConfigurable = FuzzierSettingsConfigurable() - settingsConfigurable.createComponent() + @Test + fun `Configurable is instanced with no changes`() { + pre() assertFalse(settingsConfigurable.isModified()) } @Test - fun `Test is modified with a single change`() { - state.exclusionSet = setOf("Hello", "There") - state.newTab = true - state.recentFilesMode = RECENT_PROJECT_FILES - state.debouncePeriod = 140 - state.fileListLimit = 200 + fun exclusionSet() { + pre() + state.exclusionSet = setOf("Hello", "There", "World") + assertTrue(settingsConfigurable.isModified()) + } - state.filenameType = FILENAME_WITH_PATH_STYLED - state.highlightFilename = false - state.fileListFontSize = 15 - state.previewFontSize = 0 - state.fileListSpacing = 2 + @Test + fun newTab() { + pre() + state.newTab = false + assertTrue(settingsConfigurable.isModified()) + } - state.tolerance = 4 - state.multiMatch = true - state.matchWeightPartialPath = 8 - state.matchWeightSingleChar = 6 - state.matchWeightStreakModifier = 20 - state.matchWeightFilename = 15 + @Test + fun recentFilesMode() { + pre() + state.recentFilesMode = NONE + assertTrue(settingsConfigurable.isModified()) + } - val settingsConfigurable = FuzzierSettingsConfigurable() - settingsConfigurable.createComponent() + @Test + fun prioritizeShorterDirPaths() { + pre() + state.prioritizeShorterDirPaths = true + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun debouncePeriod() { + pre() + state.debouncePeriod = 150 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun fileListLimit() { + pre() + state.fileListLimit = 250 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun filenameType() { + pre() + state.filenameType = FuzzyMatchContainer.FilenameType.FILENAME_ONLY + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun highlightFilename() { + pre() + state.highlightFilename = true + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun fileListFontSize() { + pre() state.fileListFontSize = 16 assertTrue(settingsConfigurable.isModified()) } + + @Test + fun previewFontSize() { + pre() + state.previewFontSize = 14 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun fileListSpacing() { + pre() + state.fileListSpacing = 3 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun tolerance() { + pre() + state.tolerance = 5 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun multiMatch() { + pre() + state.multiMatch = false + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun matchWeightPartialPath() { + pre() + state.matchWeightPartialPath = 9 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun matchWeightSingleChar() { + pre() + state.matchWeightSingleChar = 7 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun matchWeightStreakModifier() { + pre() + state.matchWeightStreakModifier = 21 + assertTrue(settingsConfigurable.isModified()) + } + + @Test + fun matchWeightFilename() { + pre() + state.matchWeightFilename = 16 + assertTrue(settingsConfigurable.isModified()) + } + + private fun pre() { + settingsConfigurable.createComponent() + assertFalse(settingsConfigurable.isModified()) + } } \ 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 f32686b9..199cdf54 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/util/FuzzierUtilTest.kt @@ -97,18 +97,18 @@ class FuzzierUtilTest { assertEquals(3, modules.size) var file = myFixture.findFileInTempDir("/src1/file1") - assertEquals("/src1/file1", fuzzierUtil.removeModulePath(file.path).first) - var finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src1/file1", fuzzierUtil.extractModulePath(file.path).first) + var finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("unitTest")) file = myFixture.findFileInTempDir("/src1/module1/file1") - assertEquals("/src1/module1/file1", fuzzierUtil.removeModulePath(file.path).first) - finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src1/module1/file1", fuzzierUtil.extractModulePath(file.path).first) + finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("unitTest")) file = myFixture.findFileInTempDir("/src2/file1") - assertEquals("/src2/file1", fuzzierUtil.removeModulePath(file.path).first) - finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src2/file1", fuzzierUtil.extractModulePath(file.path).first) + finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("unitTest")) } @@ -121,18 +121,18 @@ class FuzzierUtilTest { assertEquals(3, modules.size) var file = myFixture.findFileInTempDir("/path/src1/file1") - assertEquals("/src1/file1", fuzzierUtil.removeModulePath(file.path).first) - var finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src1/file1", fuzzierUtil.extractModulePath(file.path).first) + var finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("path")) file = myFixture.findFileInTempDir("/to/src2/file2") - assertEquals("/src2/file2", fuzzierUtil.removeModulePath(file.path).first) - finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src2/file2", fuzzierUtil.extractModulePath(file.path).first) + finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("to")) file = myFixture.findFileInTempDir("/module/src3/file3") - assertEquals("/src3/file3", fuzzierUtil.removeModulePath(file.path).first) - finalPath = fuzzierUtil.removeModulePath(file.path).second.substringAfterLast("/"); + assertEquals("/src3/file3", fuzzierUtil.extractModulePath(file.path).first) + finalPath = fuzzierUtil.extractModulePath(file.path).second.substringAfterLast("/"); assertTrue(finalPath.startsWith("module")) } @@ -145,7 +145,7 @@ class FuzzierUtilTest { assertEquals(1, modules.size) val file = myFixture.findFileInTempDir("/path/src1/file1") - assertEquals("/file1", fuzzierUtil.removeModulePath(file.path).first) + assertEquals("/file1", fuzzierUtil.extractModulePath(file.path).first) } @Test @@ -156,7 +156,7 @@ class FuzzierUtilTest { val modules = service().state.modules assertEquals(1, modules.size) - assertEquals(Pair("/no/such/file", ""), fuzzierUtil.removeModulePath("/no/such/file")) + assertEquals(Pair("/no/such/file", ""), fuzzierUtil.extractModulePath("/no/such/file")) } @Test diff --git a/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt b/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt new file mode 100644 index 00000000..962365a7 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/util/InitialViewHandlerTest.kt @@ -0,0 +1,201 @@ +/* +MIT License + +Copyright (c) 2024 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.fileEditor.impl.EditorHistoryManager +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.settings.FuzzierSettingsService +import com.mituuz.fuzzier.settings.FuzzierSettingsService.State +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import javax.swing.DefaultListModel + +class InitialViewHandlerTest { + private lateinit var project: Project + private lateinit var fuzzierSettingsService: FuzzierSettingsService + private lateinit var fuzzierUtil: FuzzierUtil + private lateinit var initialViewHandler: InitialViewHandler + private lateinit var state: State + private lateinit var editorHistoryManager: EditorHistoryManager + @Suppress("unused") // Required for add to recently used files (fuzzierSettingsServiceInstance) + private var testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance() + + @BeforeEach + fun setUp() { + project = mock(Project::class.java) + fuzzierSettingsService = mock(FuzzierSettingsService::class.java) + state = mock(State::class.java) + fuzzierUtil = mock(FuzzierUtil::class.java) + initialViewHandler = InitialViewHandler() + editorHistoryManager = mock(EditorHistoryManager::class.java) + `when`(fuzzierSettingsService.state).thenReturn(state) + } + + @Test + fun `Recent project files - Verify that list is truncated when it goes over the file limit`() { + val virtualFile1 = mock(VirtualFile::class.java) + val virtualFile2 = mock(VirtualFile::class.java) + val fileList = listOf( + virtualFile1, + virtualFile2 + ) + `when`(editorHistoryManager.fileList).thenReturn(fileList) + `when`(fuzzierSettingsService.state.fileListLimit).thenReturn(1) + `when`(virtualFile1.path).thenReturn("path") + `when`(virtualFile1.name).thenReturn("filename1") + `when`(virtualFile2.path).thenReturn("path") + `when`(virtualFile2.name).thenReturn("filename2") + `when`(fuzzierUtil.extractModulePath(anyString())).thenReturn(Pair("path", "module")) + + val result = InitialViewHandler.getRecentProjectFiles(fuzzierSettingsService, fuzzierUtil, editorHistoryManager) + + assertEquals(1, result.size()) + } + + @Test + fun `Recent project files - Skip files that do not belong to the project`() { + val virtualFile1 = mock(VirtualFile::class.java) + val virtualFile2 = mock(VirtualFile::class.java) + val fileList = listOf( + virtualFile1, + virtualFile2 + ) + `when`(editorHistoryManager.fileList).thenReturn(fileList) + `when`(fuzzierSettingsService.state.fileListLimit).thenReturn(2) + `when`(virtualFile1.path).thenReturn("path") + `when`(virtualFile1.name).thenReturn("filename1") + `when`(virtualFile2.path).thenReturn("path") + `when`(virtualFile2.name).thenReturn("filename2") + `when`(fuzzierUtil.extractModulePath(anyString())).thenReturn(Pair("path", "module"), Pair("", "")) + + val result = InitialViewHandler.getRecentProjectFiles(fuzzierSettingsService, fuzzierUtil, editorHistoryManager) + + assertEquals(1, result.size()) + } + + @Test + fun `Recent project files - Empty list when no history`() { + `when`(editorHistoryManager.fileList).thenReturn(emptyList()) + `when`(fuzzierSettingsService.state.fileListLimit).thenReturn(2) + + val result = InitialViewHandler.getRecentProjectFiles(fuzzierSettingsService, fuzzierUtil, editorHistoryManager) + + assertEquals(0, result.size()) + } + + @Test + fun `Recently searched files - Null returns an empty list`() { + `when`(fuzzierSettingsService.state.recentlySearchedFiles).thenReturn(null) + val result = InitialViewHandler.getRecentlySearchedFiles(fuzzierSettingsService) + assertEquals(0, result.size()) + } + + @Test + fun `Recently searched files - Order of multiple files`() { + val fuzzyMatchContainer1 = mock(FuzzyMatchContainer::class.java) + val fuzzyMatchContainer2 = mock(FuzzyMatchContainer::class.java) + val listModel = DefaultListModel() + listModel.addElement(fuzzyMatchContainer1) + listModel.addElement(fuzzyMatchContainer2) + `when`(fuzzierSettingsService.state.recentlySearchedFiles).thenReturn(listModel) + + val result = InitialViewHandler.getRecentlySearchedFiles(fuzzierSettingsService) + + assertEquals(fuzzyMatchContainer2, result[0]) + assertEquals(fuzzyMatchContainer1, result[1]) + } + + @Test + fun `Recently searched files - Remove null elements from the list`() { + val fuzzyMatchContainer = mock(FuzzyMatchContainer::class.java) + val listModel = DefaultListModel() + listModel.addElement(fuzzyMatchContainer) + listModel.addElement(null) + listModel.addElement(null) + `when`(fuzzierSettingsService.state.recentlySearchedFiles).thenReturn(listModel) + + val result = InitialViewHandler.getRecentlySearchedFiles(fuzzierSettingsService) + + assertEquals(1, result.size) + } + + @Test + fun `Add file to recently used files - Null list should default to empty`() { + val fuzzierSettingsServiceInstance: FuzzierSettingsService = service() + val score = FuzzyMatchContainer.FuzzyScore() + val container = FuzzyMatchContainer(score, "", "") + + fuzzierSettingsServiceInstance.state.recentlySearchedFiles = null + InitialViewHandler.addFileToRecentlySearchedFiles(container, fuzzierSettingsServiceInstance) + assertNotNull(fuzzierSettingsServiceInstance.state.recentlySearchedFiles) + assertEquals(1, fuzzierSettingsServiceInstance.state.recentlySearchedFiles!!.size) + } + + @Test + fun `Add file to recently used files - Too large list is truncated`() { + val fuzzierSettingsServiceInstance: FuzzierSettingsService = service() + val fileListLimit = 2 + val score = FuzzyMatchContainer.FuzzyScore() + val container = FuzzyMatchContainer(score, "", "") + + val largeList: DefaultListModel = DefaultListModel() + for (i in 0..25) { + largeList.addElement(FuzzyMatchContainer(score, "" + i, "" + i)) + } + + fuzzierSettingsServiceInstance.state.fileListLimit = fileListLimit + + fuzzierSettingsServiceInstance.state.recentlySearchedFiles = largeList + InitialViewHandler.addFileToRecentlySearchedFiles(container, fuzzierSettingsServiceInstance) + assertEquals(fileListLimit, fuzzierSettingsServiceInstance.state.recentlySearchedFiles!!.size) + } + + @Test + fun `Add file to recently used files - Duplicate filenames are removed`() { + val fuzzierSettingsServiceInstance: FuzzierSettingsService = service() + val fileListLimit = 20 + val score = FuzzyMatchContainer.FuzzyScore() + val container = FuzzyMatchContainer(score, "", "") + + val largeList: DefaultListModel = DefaultListModel() + repeat (26) { + largeList.addElement(FuzzyMatchContainer(score, "", "")) + } + + fuzzierSettingsServiceInstance.state.fileListLimit = fileListLimit + + fuzzierSettingsServiceInstance.state.recentlySearchedFiles = largeList + InitialViewHandler.addFileToRecentlySearchedFiles(container, fuzzierSettingsServiceInstance) + assertEquals(1, fuzzierSettingsServiceInstance.state.recentlySearchedFiles!!.size) + } +} \ No newline at end of file