From 4a012a9769510581229702600b07d63ce101cf5b Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 18:15:23 +0300 Subject: [PATCH 1/7] Remove redundant IO dispatch and cleanup --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 528d928d..5fb00354 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -132,25 +132,32 @@ open class FuzzyGrep : FuzzyAction() { override fun updateListContents(project: Project, searchString: String) { if (StringUtils.isBlank(searchString)) { + currentUpdateListContentJob?.cancel() + currentUpdateListContentJob = null component.fileList.model = DefaultListModel() + component.fileList.setPaintBusy(false) return } currentUpdateListContentJob?.cancel() - currentUpdateListContentJob = actionScope?.launch(Dispatchers.EDT) { + val updateJob = actionScope?.launch(Dispatchers.EDT, start = CoroutineStart.LAZY) { component.fileList.setPaintBusy(true) + try { - val results = withContext(Dispatchers.IO) { - findInFiles( - searchString, project - ) - } + val results = findInFiles(searchString, project) coroutineContext.ensureActive() + component.refreshModel(results, getCellRenderer()) } finally { - component.fileList.setPaintBusy(false) + if (currentUpdateListContentJob == coroutineContext.job) { + component.fileList.setPaintBusy(false) + currentUpdateListContentJob = null + } } } + + currentUpdateListContentJob = updateJob + updateJob?.start() } private suspend fun findInFiles( @@ -159,12 +166,16 @@ open class FuzzyGrep : FuzzyAction() { ): ListModel { val listModel = DefaultListModel() val projectBasePath = project.basePath + val resolvedBackend = backend - if (backend != null && projectBasePath != null) { + if (resolvedBackend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() - backend!!.handleSearch( + val changelistManager = ChangeListManager.getInstance(project) + resolvedBackend.handleSearch( grepConfig, searchString, secondaryFieldText, commandRunner, listModel, projectBasePath, project - ) { vf -> validVf(vf, secondaryFieldText, ChangeListManager.getInstance(project)) } + ) { vf -> + validVf(vf, secondaryFieldText, changelistManager) + } } return listModel From 1abd80f93d05080b9bf24b7f0dcffee49621475c Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:02:53 +0300 Subject: [PATCH 2/7] Add some tests for file validation --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 2 +- .../com/mituuz/fuzzier/grep/FuzzyGrepTest.kt | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 5fb00354..e274296d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -181,7 +181,7 @@ open class FuzzyGrep : FuzzyAction() { return listModel } - private fun validVf( + fun validVf( virtualFile: VirtualFile, secondaryFieldText: String? = null, clm: ChangeListManager ): Boolean { if (virtualFile.isDirectory) return false diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt new file mode 100644 index 00000000..6c89f822 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -0,0 +1,122 @@ +/* + * 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.grep + +import com.intellij.openapi.vcs.changes.ChangeListManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.TestApplicationManager +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class FuzzyGrepTest { + private lateinit var fGrep: FuzzyGrep + + @BeforeEach + fun setUp() { + TestApplicationManager.getInstance() + fGrep = FuzzyGrep() + } + + @Test + fun `Directories should not be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns true + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `Binary files should not be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns false + every { file1.fileType.isBinary } returns true + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `Ignored files should not be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns false + every { file1.fileType.isBinary } returns false + every { clm.isIgnoredFile(file1) } returns true + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `null secondary field should be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns false + every { file1.fileType.isBinary } returns false + every { clm.isIgnoredFile(file1) } returns false + + val res = fGrep.validVf(file1, null, clm) + assert(res) + } + + @Test + fun `Matching secondary field should be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns false + every { file1.fileType.isBinary } returns false + every { clm.isIgnoredFile(file1) } returns false + + every { file1.extension } returns "kt" + + val res = fGrep.validVf(file1, "kt", clm) + assert(res) + } + + @Test + fun `Non-matching secondary field should not be valid`() { + val file1 = mockk() + val clm = mockk() + + every { file1.isDirectory } returns false + every { file1.fileType.isBinary } returns false + every { clm.isIgnoredFile(file1) } returns false + + every { file1.extension } returns "java" + + val res = fGrep.validVf(file1, "kt", clm) + assert(!res) + } +} \ No newline at end of file From b4a42cdd1e0519fd62f64a312a3cdaeadcbc6587 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:05:33 +0300 Subject: [PATCH 3/7] Add unmockk between the tests --- src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt index 6c89f822..f513b728 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -29,6 +29,8 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.TestApplicationManager import io.mockk.every import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -41,6 +43,11 @@ class FuzzyGrepTest { fGrep = FuzzyGrep() } + @AfterEach + fun tearDown() { + unmockkAll() + } + @Test fun `Directories should not be valid`() { val file1 = mockk() From a50db6706c96751c128091382a4fb9546de4575a Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:10:08 +0300 Subject: [PATCH 4/7] Pass instances for testing --- .../kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index e274296d..1ac6caad 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -144,7 +144,13 @@ open class FuzzyGrep : FuzzyAction() { component.fileList.setPaintBusy(true) try { - val results = findInFiles(searchString, project) + val changelistManager = ChangeListManager.getInstance(project) + val results = findInFiles( + searchString, + project, + changelistManager, + backend + ) coroutineContext.ensureActive() component.refreshModel(results, getCellRenderer()) @@ -160,21 +166,21 @@ open class FuzzyGrep : FuzzyAction() { updateJob?.start() } - private suspend fun findInFiles( + suspend fun findInFiles( searchString: String, project: Project, + clm: ChangeListManager, + resolvedBackend: BackendStrategy? ): ListModel { val listModel = DefaultListModel() val projectBasePath = project.basePath - val resolvedBackend = backend if (resolvedBackend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() - val changelistManager = ChangeListManager.getInstance(project) resolvedBackend.handleSearch( grepConfig, searchString, secondaryFieldText, commandRunner, listModel, projectBasePath, project ) { vf -> - validVf(vf, secondaryFieldText, changelistManager) + validVf(vf, secondaryFieldText, clm) } } From 1775746e55f56dd5733ad38c90d1182d4289eb05 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:13:57 +0300 Subject: [PATCH 5/7] Add initial test setup for findInFiles --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 10 ++++++- .../com/mituuz/fuzzier/grep/FuzzyGrepTest.kt | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 1ac6caad..18482174 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -92,7 +92,7 @@ open class FuzzyGrep : FuzzyAction() { } val resolvedBackend = backendResult.getOrNull() ?: return@launch - backend = resolvedBackend + updateBackend(resolvedBackend) val popupTitle = grepConfig.getPopupTitle(resolvedBackend.name) yield() @@ -239,4 +239,12 @@ open class FuzzyGrep : FuzzyAction() { } } } + + fun updateBackend(resolvedBackend: BackendStrategy?) { + backend = resolvedBackend + } + + fun updateGrepConfig(config: GrepConfig) { + grepConfig = config + } } diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt index f513b728..3de94e32 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -24,13 +24,21 @@ package com.mituuz.fuzzier.grep +import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.changes.ChangeListManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.TestApplicationManager +import com.mituuz.fuzzier.components.FuzzyFinderComponent +import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.GrepConfig +import com.mituuz.fuzzier.grep.backend.BackendStrategy import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.unmockkAll import org.junit.jupiter.api.AfterEach +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 @@ -126,4 +134,25 @@ class FuzzyGrepTest { val res = fGrep.validVf(file1, "kt", clm) assert(!res) } + + @Test + fun `findInFiles test frame should setup required mocks`() { + val project = mockk() + val component = mockk() + val backend = mockk() + val changelistManager = mockk() + + mockkStatic(ChangeListManager::class) + every { project.basePath } returns "/tmp/project" + every { component.getSecondaryText() } returns "kt" + every { ChangeListManager.getInstance(project) } returns changelistManager + + fGrep.component = component + fGrep.updateBackend(backend) + fGrep.updateGrepConfig(GrepConfig(targets = null, caseMode = CaseMode.SENSITIVE, title = "Fuzzy Grep")) + + assertEquals("/tmp/project", project.basePath) + assertEquals("kt", component.getSecondaryText()) + assertNotNull(ChangeListManager.getInstance(project)) + } } \ No newline at end of file From ca1656035766e678a5cfb780b3902868d7478a3e Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:15:27 +0300 Subject: [PATCH 6/7] Extract the common part for tests --- .../com/mituuz/fuzzier/grep/FuzzyGrepTest.kt | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt index 3de94e32..5bb2e13d 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -45,6 +45,11 @@ import org.junit.jupiter.api.Test class FuzzyGrepTest { private lateinit var fGrep: FuzzyGrep + private data class ValidVfContext( + val file: VirtualFile, + val clm: ChangeListManager + ) + @BeforeEach fun setUp() { TestApplicationManager.getInstance() @@ -56,12 +61,28 @@ class FuzzyGrepTest { unmockkAll() } - @Test - fun `Directories should not be valid`() { - val file1 = mockk() + private fun createValidVfContext( + isDirectory: Boolean = false, + isBinary: Boolean = false, + isIgnored: Boolean = false, + extension: String? = null + ): ValidVfContext { + val file = mockk() val clm = mockk() - every { file1.isDirectory } returns true + every { file.isDirectory } returns isDirectory + every { file.fileType.isBinary } returns isBinary + every { clm.isIgnoredFile(file) } returns isIgnored + if (extension != null) { + every { file.extension } returns extension + } + + return ValidVfContext(file, clm) + } + + @Test + fun `Directories should not be valid`() { + val (file1, clm) = createValidVfContext(isDirectory = true) val res = fGrep.validVf(file1, null, clm) assert(!res) @@ -69,11 +90,7 @@ class FuzzyGrepTest { @Test fun `Binary files should not be valid`() { - val file1 = mockk() - val clm = mockk() - - every { file1.isDirectory } returns false - every { file1.fileType.isBinary } returns true + val (file1, clm) = createValidVfContext(isBinary = true) val res = fGrep.validVf(file1, null, clm) assert(!res) @@ -81,12 +98,7 @@ class FuzzyGrepTest { @Test fun `Ignored files should not be valid`() { - val file1 = mockk() - val clm = mockk() - - every { file1.isDirectory } returns false - every { file1.fileType.isBinary } returns false - every { clm.isIgnoredFile(file1) } returns true + val (file1, clm) = createValidVfContext(isIgnored = true) val res = fGrep.validVf(file1, null, clm) assert(!res) @@ -94,12 +106,7 @@ class FuzzyGrepTest { @Test fun `null secondary field should be valid`() { - val file1 = mockk() - val clm = mockk() - - every { file1.isDirectory } returns false - every { file1.fileType.isBinary } returns false - every { clm.isIgnoredFile(file1) } returns false + val (file1, clm) = createValidVfContext() val res = fGrep.validVf(file1, null, clm) assert(res) @@ -107,14 +114,7 @@ class FuzzyGrepTest { @Test fun `Matching secondary field should be valid`() { - val file1 = mockk() - val clm = mockk() - - every { file1.isDirectory } returns false - every { file1.fileType.isBinary } returns false - every { clm.isIgnoredFile(file1) } returns false - - every { file1.extension } returns "kt" + val (file1, clm) = createValidVfContext(extension = "kt") val res = fGrep.validVf(file1, "kt", clm) assert(res) @@ -122,14 +122,7 @@ class FuzzyGrepTest { @Test fun `Non-matching secondary field should not be valid`() { - val file1 = mockk() - val clm = mockk() - - every { file1.isDirectory } returns false - every { file1.fileType.isBinary } returns false - every { clm.isIgnoredFile(file1) } returns false - - every { file1.extension } returns "java" + val (file1, clm) = createValidVfContext(extension = "java") val res = fGrep.validVf(file1, "kt", clm) assert(!res) From 1ad28ac3434afaeb8010fd1481a02c31bcb15732 Mon Sep 17 00:00:00 2001 From: Mitja Date: Fri, 8 May 2026 19:22:35 +0300 Subject: [PATCH 7/7] Add basic test for find in files --- .../com/mituuz/fuzzier/grep/FuzzyGrepTest.kt | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt index 5bb2e13d..0341a090 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -34,8 +34,8 @@ import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.grep.backend.BackendStrategy import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.unmockkAll +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull @@ -50,6 +50,13 @@ class FuzzyGrepTest { val clm: ChangeListManager ) + private data class FindInFilesContext( + val project: Project, + val component: FuzzyFinderComponent, + val backend: BackendStrategy, + val clm: ChangeListManager + ) + @BeforeEach fun setUp() { TestApplicationManager.getInstance() @@ -80,6 +87,27 @@ class FuzzyGrepTest { return ValidVfContext(file, clm) } + private fun createFindInFilesContext( + projectBasePath: String? = "/tmp/project", + secondaryText: String = "kt" + ): FindInFilesContext { + val project = mockk() + val component = mockk() + val backend = mockk() + val clm = mockk() + + every { project.basePath } returns projectBasePath + every { component.getSecondaryText() } returns secondaryText + + fGrep.component = component + fGrep.updateBackend(backend) + fGrep.updateGrepConfig( + GrepConfig(targets = null, caseMode = CaseMode.SENSITIVE, title = "Fuzzy Grep") + ) + + return FindInFilesContext(project, component, backend, clm) + } + @Test fun `Directories should not be valid`() { val (file1, clm) = createValidVfContext(isDirectory = true) @@ -129,23 +157,22 @@ class FuzzyGrepTest { } @Test - fun `findInFiles test frame should setup required mocks`() { - val project = mockk() - val component = mockk() - val backend = mockk() - val changelistManager = mockk() + fun `findInFiles should skip backend when backend is null`() = runBlocking { + val (project, _, _, clm) = createFindInFilesContext() - mockkStatic(ChangeListManager::class) - every { project.basePath } returns "/tmp/project" - every { component.getSecondaryText() } returns "kt" - every { ChangeListManager.getInstance(project) } returns changelistManager + val model = fGrep.findInFiles("needle", project, clm, null) - fGrep.component = component - fGrep.updateBackend(backend) - fGrep.updateGrepConfig(GrepConfig(targets = null, caseMode = CaseMode.SENSITIVE, title = "Fuzzy Grep")) + assertNotNull(model) + assertEquals(0, model.size) + } + + @Test + fun `findInFiles should skip backend when project base path is null`() = runBlocking { + val (project, _, backend, clm) = createFindInFilesContext(projectBasePath = null) + + val model = fGrep.findInFiles("needle", project, clm, backend) - assertEquals("/tmp/project", project.basePath) - assertEquals("kt", component.getSecondaryText()) - assertNotNull(ChangeListManager.getInstance(project)) + assertNotNull(model) + assertEquals(0, model.size) } } \ No newline at end of file