diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 528d928d..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() @@ -132,45 +132,62 @@ 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 changelistManager = ChangeListManager.getInstance(project) + val results = findInFiles( + searchString, + project, + changelistManager, + backend + ) 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( + suspend fun findInFiles( searchString: String, project: Project, + clm: ChangeListManager, + resolvedBackend: BackendStrategy? ): ListModel { val listModel = DefaultListModel() val projectBasePath = project.basePath - if (backend != null && projectBasePath != null) { + if (resolvedBackend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() - backend!!.handleSearch( + resolvedBackend.handleSearch( grepConfig, searchString, secondaryFieldText, commandRunner, listModel, projectBasePath, project - ) { vf -> validVf(vf, secondaryFieldText, ChangeListManager.getInstance(project)) } + ) { vf -> + validVf(vf, secondaryFieldText, clm) + } } return listModel } - private fun validVf( + fun validVf( virtualFile: VirtualFile, secondaryFieldText: String? = null, clm: ChangeListManager ): Boolean { if (virtualFile.isDirectory) return false @@ -222,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 new file mode 100644 index 00000000..0341a090 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepTest.kt @@ -0,0 +1,178 @@ +/* + * 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.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.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 +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class FuzzyGrepTest { + private lateinit var fGrep: FuzzyGrep + + private data class ValidVfContext( + val file: VirtualFile, + val clm: ChangeListManager + ) + + private data class FindInFilesContext( + val project: Project, + val component: FuzzyFinderComponent, + val backend: BackendStrategy, + val clm: ChangeListManager + ) + + @BeforeEach + fun setUp() { + TestApplicationManager.getInstance() + fGrep = FuzzyGrep() + } + + @AfterEach + fun tearDown() { + unmockkAll() + } + + private fun createValidVfContext( + isDirectory: Boolean = false, + isBinary: Boolean = false, + isIgnored: Boolean = false, + extension: String? = null + ): ValidVfContext { + val file = mockk() + val clm = mockk() + + 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) + } + + 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) + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `Binary files should not be valid`() { + val (file1, clm) = createValidVfContext(isBinary = true) + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `Ignored files should not be valid`() { + val (file1, clm) = createValidVfContext(isIgnored = true) + + val res = fGrep.validVf(file1, null, clm) + assert(!res) + } + + @Test + fun `null secondary field should be valid`() { + val (file1, clm) = createValidVfContext() + + val res = fGrep.validVf(file1, null, clm) + assert(res) + } + + @Test + fun `Matching secondary field should be valid`() { + val (file1, clm) = createValidVfContext(extension = "kt") + + val res = fGrep.validVf(file1, "kt", clm) + assert(res) + } + + @Test + fun `Non-matching secondary field should not be valid`() { + val (file1, clm) = createValidVfContext(extension = "java") + + val res = fGrep.validVf(file1, "kt", clm) + assert(!res) + } + + @Test + fun `findInFiles should skip backend when backend is null`() = runBlocking { + val (project, _, _, clm) = createFindInFilesContext() + + val model = fGrep.findInFiles("needle", project, clm, null) + + 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) + + assertNotNull(model) + assertEquals(0, model.size) + } +} \ No newline at end of file