From ea0103fbe7ffb914fb72f953d50d750080fec378 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 08:50:18 +0200 Subject: [PATCH 01/31] Verify that project base path exists before using it as the working dir --- src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 88a7ea37..65ca66cf 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -172,9 +172,9 @@ open class FuzzyGrep : FuzzyAction() { project: Project, ): ListModel { val listModel = DefaultListModel() - val projectBasePath = project.basePath.toString() + val projectBasePath = project.basePath - if (backend != null) { + if (backend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() val commands = backend!!.buildCommand(grepConfig, searchString, secondaryFieldText) commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, backend!!) From 20beffea091ca703f37a6567e809f2fe38bc4153 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 08:59:04 +0200 Subject: [PATCH 02/31] Change API to enable kotlin only backend --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 10 ++++- .../fuzzier/grep/backend/BackendStrategy.kt | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 65ca66cf..17d696f9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -176,8 +176,14 @@ open class FuzzyGrep : FuzzyAction() { if (backend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() - val commands = backend!!.buildCommand(grepConfig, searchString, secondaryFieldText) - commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, backend!!) + backend!!.handleSearch( + grepConfig, + searchString, + secondaryFieldText, + commandRunner, + listModel, + projectBasePath, + ) } return listModel diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 3cf53d48..f56d5ab2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -25,8 +25,11 @@ package com.mituuz.fuzzier.grep.backend import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer +import com.mituuz.fuzzier.runner.CommandRunner +import javax.swing.DefaultListModel sealed interface BackendStrategy { val name: String @@ -36,6 +39,18 @@ sealed interface BackendStrategy { return RowContainer.rowContainerFromString(line, projectBasePath) } + suspend fun handleSearch( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String?, + commandRunner: CommandRunner, + listModel: DefaultListModel, + projectBasePath: String, + ) { + val commands = buildCommand(grepConfig, searchString, secondarySearchString) + commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, this) + } + fun supportsSecondaryField(): Boolean = false object Ripgrep : BackendStrategy { @@ -135,5 +150,28 @@ sealed interface BackendStrategy { return commands } } + + object Fuzzier : BackendStrategy { + override val name = "fuzzier" + + override fun buildCommand( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String? + ): List { + TODO("Not yet implemented") + } + + override suspend fun handleSearch( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String?, + commandRunner: CommandRunner, + listModel: DefaultListModel, + projectBasePath: String + ) { + TODO("Not yet implemented") + } + } } From 2361f21897287c772b5e07d18f683f9ccf434c2e Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 08:59:58 +0200 Subject: [PATCH 03/31] Return emptylist --- .../kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index f56d5ab2..f7459d18 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -158,9 +158,7 @@ sealed interface BackendStrategy { grepConfig: GrepConfig, searchString: String, secondarySearchString: String? - ): List { - TODO("Not yet implemented") - } + ): List = emptyList() override suspend fun handleSearch( grepConfig: GrepConfig, From f9bba183641ebd5f2ba3df7b31edc574428db278 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 09:10:00 +0200 Subject: [PATCH 04/31] Add plan for fuzzier grep and modify the interface signature --- fuzzier-grep.md | 53 +++++++++++++++++++ .../fuzzier/grep/backend/BackendStrategy.kt | 8 ++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 fuzzier-grep.md diff --git a/fuzzier-grep.md b/fuzzier-grep.md new file mode 100644 index 00000000..3af51421 --- /dev/null +++ b/fuzzier-grep.md @@ -0,0 +1,53 @@ +# Pure Kotlin Grep Backend Plan (The "Fuzzier" Strategy) + +## 1. Core APIs to Leverage + +- **`CacheManager.getInstance(project).getFilesWithWord()`**: Find files indexed with specific words. +- **`ChangeListManager.getInstance(project)`**: Check if a file is ignored by VCS (e.g., via `.gitignore`). +- **`GlobalSearchScope`**: Restrict searches to the project scope, which naturally excludes library sources and excluded + folders. +- **`ReadAction.nonBlocking`**: Ensures thread-safe index access that cancels automatically on user input. + +## 2. The Hybrid Search Strategy + +### Case A: Short String (Length < 3) + +* **Action**: Use a linear iteration (like your `IntelliJIterationFileCollector`). +* **VCS Check**: For every file encountered, check `ChangeListManager.isIgnoredFile(vf)`. Skip if `true`. + +### Case B: Long String (Length >= 3) + +* **Action**: Use `CacheManager.getFilesWithWord()`. +* **VCS Check**: + +1. The `GlobalSearchScope.projectScope(project)` already excludes many non-project files. +2. Filter the resulting `VirtualFile[]` against `ChangeListManager.isIgnoredFile(vf)`. + +## 3. Passing the VCS Filter + +To make this work cleanly, we should update `GrepConfig` or the `handleSearch` signature to include a file filter. This +allows `FuzzierVCS` (or any other action) to tell the backend exactly what to ignore. + +1. **Define a Filter**: In `FuzzyGrep`, create a `fileFilter: (VirtualFile) -> Boolean`. +2. **VCS Logic**: Inside the filter, use `!ChangeListManager.getInstance(project).isIgnoredFile(vf)`. +3. **Backend Execution**: The `Fuzzier` backend should apply this filter before reading file contents. + +## 4. Implementation Steps for `Fuzzier.handleSearch` + +1. **Get Project instance**: Since `BackendStrategy` doesn't have it, we should add `project: Project` to the + `handleSearch` parameters. +2. **Check Length**: + +- If `searchString.length < 3`: Manually iterate project files using the VCS-aware filter. +- If `>= 3`: Query `CacheManager` using `GlobalSearchScope.projectScope(project)`, then filter results by VCS status and + `grepConfig.targets`. + +3. **Read & Match**: For valid files, load text, find matches, and batch-add to `listModel`. +4. **Batch & Cancel**: Use `withContext(Dispatchers.Main)` for batching and `ensureActive()` for cancellation. +5. **Limit**: Stop at 1000 results. + +## 5. Performance Tips + +- **Scope is Key**: `GlobalSearchScope` is very powerful; it can be restricted to specific modules or directories, which + maps perfectly to your `grepConfig.targets`. +- **Read Action**: Always perform `CacheManager` and `ChangeListManager` checks inside a `ReadAction`. diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index f7459d18..8dc8a299 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -24,6 +24,8 @@ package com.mituuz.fuzzier.grep.backend +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.entities.CaseMode import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig @@ -46,6 +48,8 @@ sealed interface BackendStrategy { commandRunner: CommandRunner, listModel: DefaultListModel, projectBasePath: String, + project: Project? = null, + fileFilter: (VirtualFile) -> Boolean = { true } ) { val commands = buildCommand(grepConfig, searchString, secondarySearchString) commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, this) @@ -166,7 +170,9 @@ sealed interface BackendStrategy { secondarySearchString: String?, commandRunner: CommandRunner, listModel: DefaultListModel, - projectBasePath: String + projectBasePath: String, + project: Project?, + fileFilter: (VirtualFile) -> Boolean ) { TODO("Not yet implemented") } From 9a5d57cee7ef002f64b1f0925398d0a87c72f062 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 09:42:09 +0200 Subject: [PATCH 05/31] Separate the kotlin backend to its own file --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 10 +- .../fuzzier/grep/backend/BackendResolver.kt | 1 + .../fuzzier/grep/backend/BackendStrategy.kt | 22 --- .../fuzzier/grep/backend/FuzzierGrep.kt | 171 ++++++++++++++++++ 4 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 17d696f9..e270136e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -35,6 +35,8 @@ 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.vcs.changes.ChangeListManager +import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.SingleAlarm import com.mituuz.fuzzier.actions.FuzzyAction @@ -183,12 +185,18 @@ open class FuzzyGrep : FuzzyAction() { commandRunner, listModel, projectBasePath, - ) + project + ) { vf -> validVf(vf, project) } } return listModel } + private fun validVf(virtualFile: VirtualFile, project: Project): Boolean { + val clm = ChangeListManager.getInstance(project) + return !clm.isIgnoredFile(virtualFile) + } + private fun createListeners(project: Project) { // Add a listener that updates the contents of the preview pane component.fileList.addListSelectionListener { event -> diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt index f8f01570..beec49f3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt @@ -29,6 +29,7 @@ import com.mituuz.fuzzier.runner.CommandRunner class BackendResolver(val isWindows: Boolean) { suspend fun resolveBackend(commandRunner: CommandRunner, projectBasePath: String): Result { return when { + true -> Result.success(FuzzierGrep) isInstalled(commandRunner, "rg", projectBasePath) -> Result.success(BackendStrategy.Ripgrep) isWindows && isInstalled( commandRunner, diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 8dc8a299..81ea4627 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -155,27 +155,5 @@ sealed interface BackendStrategy { } } - object Fuzzier : BackendStrategy { - override val name = "fuzzier" - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List = emptyList() - - override suspend fun handleSearch( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String?, - commandRunner: CommandRunner, - listModel: DefaultListModel, - projectBasePath: String, - project: Project?, - fileFilter: (VirtualFile) -> Boolean - ) { - TODO("Not yet implemented") - } - } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt new file mode 100644 index 00000000..5484acc3 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -0,0 +1,171 @@ +/* + * 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.backend + +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.PsiSearchHelper +import com.intellij.util.Processor +import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.FuzzyContainer +import com.mituuz.fuzzier.entities.GrepConfig +import com.mituuz.fuzzier.entities.RowContainer +import com.mituuz.fuzzier.runner.CommandRunner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext +import javax.swing.DefaultListModel + +object FuzzierGrep : BackendStrategy { + override val name = "fuzzier" + + override fun buildCommand( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String? + ): List = emptyList() + + override suspend fun handleSearch( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String?, + commandRunner: CommandRunner, + listModel: DefaultListModel, + projectBasePath: String, + project: Project?, + fileFilter: (VirtualFile) -> Boolean + ) { + if (project == null) return + + val files = collectFiles(searchString, fileFilter, project, grepConfig) + var count = 0 + val batchSize = 20 + val currentBatch = mutableListOf() + + for (file in files) { + currentCoroutineContext().ensureActive() + + val matches = withContext(Dispatchers.IO) { + val content = VfsUtil.loadText(file) + val lines = content.lines() + val fileMatches = mutableListOf() + + for ((index, line) in lines.withIndex()) { + currentCoroutineContext().ensureActive() + + val found = if (grepConfig.caseMode == CaseMode.INSENSITIVE) { + line.contains(searchString, ignoreCase = true) + } else { + line.contains(searchString) + } + + if (found) { + fileMatches.add( + RowContainer( + file.path, + projectBasePath, + file.name, + index, + line.trim() + ) + ) + } + } + fileMatches + } + + for (match in matches) { + currentCoroutineContext().ensureActive() + + currentBatch.add(match) + count++ + + if (currentBatch.size >= batchSize) { + val toAdd = currentBatch.toList() + // currentBatch.clear() + withContext(Dispatchers.Main) { + listModel.addAll(toAdd) + } + } + + if (count >= 1000) break + } + + if (count >= 1000) break + } + + if (currentBatch.isNotEmpty()) { + withContext(Dispatchers.Main) { + listModel.addAll(currentBatch) + } + } + } + + private fun collectFiles( + searchString: String, + fileFilter: (VirtualFile) -> Boolean, + project: Project, + grepConfig: GrepConfig + ): Set { + val files = mutableSetOf() + + if (searchString.length < 3) { + // Case A: Short String (Length < 3) - Linear iteration + ReadAction.run { + ProjectFileIndex.getInstance(project).iterateContent { vf -> + if (!vf.isDirectory && fileFilter(vf)) { + files.add(vf) + } + true + } + } + } else { + // Case B: Long String (Length >= 3) - PsiSearchHelper + ReadAction.run { + val helper = PsiSearchHelper.getInstance(project) + helper.processAllFilesWithWord( + searchString, + GlobalSearchScope.projectScope(project), + Processor { psiFile -> + psiFile.virtualFile?.let { vf -> + if (fileFilter(vf)) { + files.add(vf) + } + } + true + }, + grepConfig.caseMode == CaseMode.SENSITIVE + ) + } + } + + return files + } +} \ No newline at end of file From 4cee47655fb934f7235e1df91ff0d32de286d3ae Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 21 Dec 2025 09:55:02 +0200 Subject: [PATCH 06/31] Smarter word search --- .../fuzzier/grep/backend/FuzzierGrep.kt | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 5484acc3..50c0c593 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -65,6 +65,9 @@ object FuzzierGrep : BackendStrategy { if (project == null) return val files = collectFiles(searchString, fileFilter, project, grepConfig) + + println("Searching from ${files.size} files with ${grepConfig.caseMode} case mode and $searchString") + var count = 0 val batchSize = 20 val currentBatch = mutableListOf() @@ -136,7 +139,34 @@ object FuzzierGrep : BackendStrategy { ): Set { val files = mutableSetOf() - if (searchString.length < 3) { + if (searchString.length > 3 && searchString.contains(" ")) { + // Case B: Long String (Length >= 3) - PsiSearchHelper + val words = searchString.trim().split(Regex("\\s+")) + val completeWordsSearch = if (words.size > 1) { + words.dropLast(1).joinToString(" ") + } else { + "" + } + + if (completeWordsSearch.isNotEmpty()) { + ReadAction.run { + val helper = PsiSearchHelper.getInstance(project) + helper.processAllFilesWithWord( + completeWordsSearch, + GlobalSearchScope.projectScope(project), + Processor { psiFile -> + psiFile.virtualFile?.let { vf -> + if (fileFilter(vf)) { + files.add(vf) + } + } + true + }, + grepConfig.caseMode == CaseMode.SENSITIVE + ) + } + } + } else { // Case A: Short String (Length < 3) - Linear iteration ReadAction.run { ProjectFileIndex.getInstance(project).iterateContent { vf -> @@ -146,24 +176,6 @@ object FuzzierGrep : BackendStrategy { true } } - } else { - // Case B: Long String (Length >= 3) - PsiSearchHelper - ReadAction.run { - val helper = PsiSearchHelper.getInstance(project) - helper.processAllFilesWithWord( - searchString, - GlobalSearchScope.projectScope(project), - Processor { psiFile -> - psiFile.virtualFile?.let { vf -> - if (fileFilter(vf)) { - files.add(vf) - } - } - true - }, - grepConfig.caseMode == CaseMode.SENSITIVE - ) - } } return files From 68ec2b19b704939ce52bcbdebaa2119d0ef292ff Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 13:29:59 +0200 Subject: [PATCH 07/31] Copy existing logic --- .../fuzzier/grep/backend/FuzzierGrep.kt | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 50c0c593..a6bd083f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -25,8 +25,12 @@ package com.mituuz.fuzzier.grep.backend import com.intellij.openapi.application.ReadAction +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.roots.ProjectFileIndex +import com.intellij.openapi.vcs.changes.ChangeListManager import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope @@ -37,10 +41,7 @@ import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer import com.mituuz.fuzzier.runner.CommandRunner -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import javax.swing.DefaultListModel object FuzzierGrep : BackendStrategy { @@ -131,33 +132,29 @@ object FuzzierGrep : BackendStrategy { } } - private fun collectFiles( + private suspend fun collectFiles( searchString: String, fileFilter: (VirtualFile) -> Boolean, project: Project, grepConfig: GrepConfig - ): Set { - val files = mutableSetOf() + ): List { + var files = listOf() + val trimmedSearch = searchString.trim() - if (searchString.length > 3 && searchString.contains(" ")) { + if (false) {// (trimmedSearch.contains(" ")) { // Case B: Long String (Length >= 3) - PsiSearchHelper - val words = searchString.trim().split(Regex("\\s+")) - val completeWordsSearch = if (words.size > 1) { - words.dropLast(1).joinToString(" ") - } else { - "" - } + val searchString = trimmedSearch.substringBeforeLast(" ") - if (completeWordsSearch.isNotEmpty()) { + if (searchString.isNotEmpty()) { ReadAction.run { val helper = PsiSearchHelper.getInstance(project) helper.processAllFilesWithWord( - completeWordsSearch, + searchString, GlobalSearchScope.projectScope(project), Processor { psiFile -> psiFile.virtualFile?.let { vf -> if (fileFilter(vf)) { - files.add(vf) + // files.add(vf) } } true @@ -168,16 +165,50 @@ object FuzzierGrep : BackendStrategy { } } else { // Case A: Short String (Length < 3) - Linear iteration - ReadAction.run { - ProjectFileIndex.getInstance(project).iterateContent { vf -> - if (!vf.isDirectory && fileFilter(vf)) { - files.add(vf) - } - true - } - } + files = collectIterationFiles(project) } return files } + + suspend fun collectIterationFiles(project: Project): List { + val ctx = currentCoroutineContext() + val job = ctx.job + + val indexTargets = if (false) { // (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 collectFiles( + targets = indexTargets, + shouldContinue = { job.isActive }, + fileFilter = buildFileFilter(project) + ) + } + + private fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { + val clm = ChangeListManager.getInstance(project) + return { vf -> !vf.isDirectory && !clm.isIgnoredFile(vf) } + } + + private fun collectFiles( + targets: List>, + shouldContinue: () -> Boolean, + fileFilter: (VirtualFile) -> Boolean + ): List = buildList { + for ((fileIndex, _) in targets) { + fileIndex.iterateContent { vf -> + if (!shouldContinue()) return@iterateContent false + + if (fileFilter(vf)) { + add(vf) + } + + true + } + } + } } \ No newline at end of file From fcf8409909dfe3a951dac7f826e25d698cdb5cf0 Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 13:30:56 +0200 Subject: [PATCH 08/31] Skip binary files --- src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index a6bd083f..af465142 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -191,7 +191,7 @@ object FuzzierGrep : BackendStrategy { private 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) && !vf.fileType.isBinary } } private fun collectFiles( From 24710cdc06cef62b606d77af1a6b995260ffa8d7 Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 13:33:50 +0200 Subject: [PATCH 09/31] Add timer logging --- .../com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index af465142..358c3be8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -65,14 +65,22 @@ object FuzzierGrep : BackendStrategy { ) { if (project == null) return + val fileCollectionStart = System.nanoTime() + val files = collectFiles(searchString, fileFilter, project, grepConfig) + val fileCollectionEnd = System.nanoTime() + val fileCollectionDuration = (fileCollectionEnd - fileCollectionStart) / 1_000_000.0 + println("File collection took $fileCollectionDuration ms") + println("Searching from ${files.size} files with ${grepConfig.caseMode} case mode and $searchString") var count = 0 val batchSize = 20 val currentBatch = mutableListOf() + val fileProcessingStart = System.nanoTime() + for (file in files) { currentCoroutineContext().ensureActive() @@ -125,6 +133,11 @@ object FuzzierGrep : BackendStrategy { if (count >= 1000) break } + val fileProcessingEnd = System.nanoTime() + + val fileProcessingDuration = (fileProcessingEnd - fileProcessingStart) / 1_000_000.0 + println("File processing took $fileProcessingDuration ms") + if (currentBatch.isNotEmpty()) { withContext(Dispatchers.Main) { listModel.addAll(currentBatch) From 192f6b57525ac70ed08cd9d5274917b2836a5c90 Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 13:43:21 +0200 Subject: [PATCH 10/31] Use coroutines for command and cancel command execution correctly --- .../com/mituuz/fuzzier/actions/FuzzyAction.kt | 25 +++++++++++-------- .../fuzzier/components/TestBenchComponent.kt | 14 ++++++----- .../fuzzier/grep/backend/FuzzierGrep.kt | 2 +- .../fuzzier/runner/DefaultCommandRunner.kt | 24 +++++++++++++----- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt index 482da7ad..ac4d0b73 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/actions/FuzzyAction.kt @@ -53,18 +53,15 @@ import kotlinx.coroutines.* import java.awt.Component import java.awt.Font import java.awt.event.ActionEvent -import java.util.* -import java.util.Timer import java.util.concurrent.ConcurrentHashMap import javax.swing.* -import kotlin.concurrent.schedule abstract class FuzzyAction : AnAction() { lateinit var component: FuzzyComponent lateinit var popup: JBPopup - private lateinit var originalDownHandler: EditorActionHandler - private lateinit var originalUpHandler: EditorActionHandler - private var debounceTimer: TimerTask? = null + private var originalDownHandler: EditorActionHandler? = null + private var originalUpHandler: EditorActionHandler? = null + private var debounceJob: Job? = null protected lateinit var projectState: FuzzierSettingsService.State protected val globalState = service().state protected var defaultDoc: Document? = null @@ -138,9 +135,10 @@ abstract class FuzzyAction : AnAction() { val document = component.searchField.document val listener: DocumentListener = object : DocumentListener { override fun documentChanged(event: DocumentEvent) { - debounceTimer?.cancel() + debounceJob?.cancel() val debouncePeriod = globalState.debouncePeriod - debounceTimer = Timer().schedule(debouncePeriod.toLong()) { + debounceJob = actionScope?.launch { + delay(debouncePeriod.toLong()) updateListContents(project, component.searchField.text) } } @@ -161,6 +159,9 @@ abstract class FuzzyAction : AnAction() { fun cleanupPopup() { resetOriginalHandlers() + debounceJob?.cancel() + debounceJob = null + currentUpdateListContentJob?.cancel() currentUpdateListContentJob = null @@ -180,8 +181,12 @@ abstract class FuzzyAction : AnAction() { fun resetOriginalHandlers() { val actionManager = EditorActionManager.getInstance() - actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN, originalDownHandler) - actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP, originalUpHandler) + originalDownHandler?.let { + actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN, it) + } + originalUpHandler?.let { + actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP, it) + } } class FuzzyListActionHandler(private val fuzzyAction: FuzzyAction, private val isUp: Boolean) : diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt index c98798bc..9103ea8f 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/TestBenchComponent.kt @@ -54,14 +54,13 @@ import java.util.concurrent.Future import javax.swing.DefaultListModel import javax.swing.JPanel import javax.swing.table.DefaultTableModel -import kotlin.concurrent.schedule class TestBenchComponent : JPanel(), Disposable { private val columnNames = arrayOf("Filename", "Filepath", "Streak", "MultiMatch", "PartialPath", "Filename", "Total") private val table = JBTable() private var searchField = EditorTextField() - private var debounceTimer: TimerTask? = null + private var debounceJob: Job? = null @Volatile var currentTask: Future<*>? = null @@ -122,9 +121,10 @@ class TestBenchComponent : JPanel(), Disposable { val document = searchField.document val listener: DocumentListener = object : DocumentListener { override fun documentChanged(event: DocumentEvent) { - debounceTimer?.cancel() + debounceJob?.cancel() val debouncePeriod = liveSettingsComponent.debounceTimerValue.getIntSpinner().value as Int - debounceTimer = Timer().schedule(debouncePeriod.toLong()) { + debounceJob = actionScope.launch { + delay(debouncePeriod.toLong()) updateListContents(project, searchField.text) } } @@ -199,14 +199,16 @@ class TestBenchComponent : JPanel(), Disposable { } override fun dispose() { - debounceTimer?.cancel() - debounceTimer = null + debounceJob?.cancel() + debounceJob = null currentTask?.let { task -> if (!task.isDone) task.cancel(true) } currentTask = null + actionScope.cancel() + ApplicationManager.getApplication().invokeLater { try { table.setPaintBusy(false) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 358c3be8..8edc2287 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -121,7 +121,7 @@ object FuzzierGrep : BackendStrategy { if (currentBatch.size >= batchSize) { val toAdd = currentBatch.toList() - // currentBatch.clear() + currentBatch.clear() withContext(Dispatchers.Main) { listModel.addAll(toAdd) } diff --git a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt b/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt index 283c2d91..0e16c144 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt @@ -60,9 +60,15 @@ class DefaultCommandRunner : CommandRunner { } }) - withContext(Dispatchers.IO) { - processHandler.startNotify() - processHandler.waitFor(2000) + try { + withContext(Dispatchers.IO) { + processHandler.startNotify() + processHandler.waitFor(2000) + } + } finally { + if (!processHandler.isProcessTerminated) { + processHandler.destroyProcess() + } } output.toString() } catch (_: InterruptedException) { @@ -104,9 +110,15 @@ class DefaultCommandRunner : CommandRunner { } }) - withContext(Dispatchers.IO) { - processHandler.startNotify() - processHandler.waitFor(2000) + try { + withContext(Dispatchers.IO) { + processHandler.startNotify() + processHandler.waitFor(2000) + } + } finally { + if (!processHandler.isProcessTerminated) { + processHandler.destroyProcess() + } } } catch (_: InterruptedException) { throw InterruptedException() From 0f105794116ad5827128114eb494dff16a5fa52f Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 14:05:28 +0200 Subject: [PATCH 11/31] Use pure virtual files instead of file paths --- .../mituuz/fuzzier/entities/FuzzyContainer.kt | 54 ++++++++++--------- .../mituuz/fuzzier/entities/RowContainer.kt | 6 ++- .../fuzzier/grep/backend/FuzzierGrep.kt | 23 +++++--- .../preview/CoroutinePreviewAlarmProvider.kt | 8 +-- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyContainer.kt index 527ab0c8..34ed42a6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyContainer.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyContainer.kt @@ -1,31 +1,37 @@ /* -MIT License - -Copyright (c) 2025 Mitja Leino - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ + * MIT License + * + * Copyright (c) 2025 Mitja Leino + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.mituuz.fuzzier.entities +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService -abstract class FuzzyContainer(val filePath: String, val basePath: String, val filename: String) { +abstract class FuzzyContainer( + val filePath: String, + val basePath: String, + val filename: String, + val virtualFile: VirtualFile? = null, +) { /** * Get display string for the popup */ @@ -34,7 +40,7 @@ abstract class FuzzyContainer(val filePath: String, val basePath: String, val fi /** * Get the complete URI for the file */ - fun getFileUri() : String { + fun getFileUri(): String { return "$basePath$filePath" } diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/RowContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/RowContainer.kt index 72b5871c..3ca03da9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/RowContainer.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/RowContainer.kt @@ -23,6 +23,7 @@ */ package com.mituuz.fuzzier.entities +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import java.io.File @@ -32,8 +33,9 @@ class RowContainer( filename: String, val rowNumber: Int, val trimmedRow: String, - val columnNumber: Int = 0 -) : FuzzyContainer(filePath, basePath, filename) { + val columnNumber: Int = 0, + virtualFile: VirtualFile? = null +) : FuzzyContainer(filePath, basePath, filename, virtualFile) { companion object { private val FILE_SEPARATOR: String = File.separator private val RG_PATTERN: Regex = Regex("""^.+:\d+:\d+:\s*.+$""") diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 8edc2287..03e8695c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -25,6 +25,7 @@ package com.mituuz.fuzzier.grep.backend import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.components.service import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.rootManager @@ -41,11 +42,14 @@ import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer import com.mituuz.fuzzier.runner.CommandRunner +import com.mituuz.fuzzier.settings.FuzzierSettingsService +import com.mituuz.fuzzier.util.FuzzierUtil import kotlinx.coroutines.* import javax.swing.DefaultListModel object FuzzierGrep : BackendStrategy { override val name = "fuzzier" + private val fuzzierUtil = FuzzierUtil() override fun buildCommand( grepConfig: GrepConfig, @@ -99,13 +103,15 @@ object FuzzierGrep : BackendStrategy { } if (found) { + val (filePath, basePath) = fuzzierUtil.extractModulePath(file.path, project) fileMatches.add( RowContainer( - file.path, - projectBasePath, + filePath, + basePath, file.name, index, - line.trim() + line.trim(), + virtualFile = file ) ) } @@ -151,10 +157,10 @@ object FuzzierGrep : BackendStrategy { project: Project, grepConfig: GrepConfig ): List { - var files = listOf() + val files = mutableListOf() val trimmedSearch = searchString.trim() - if (false) {// (trimmedSearch.contains(" ")) { + if (trimmedSearch.contains(" ")) { // Case B: Long String (Length >= 3) - PsiSearchHelper val searchString = trimmedSearch.substringBeforeLast(" ") @@ -167,7 +173,7 @@ object FuzzierGrep : BackendStrategy { Processor { psiFile -> psiFile.virtualFile?.let { vf -> if (fileFilter(vf)) { - // files.add(vf) + files.add(vf) } } true @@ -178,7 +184,7 @@ object FuzzierGrep : BackendStrategy { } } else { // Case A: Short String (Length < 3) - Linear iteration - files = collectIterationFiles(project) + files.addAll(collectIterationFiles(project)) } return files @@ -187,8 +193,9 @@ object FuzzierGrep : BackendStrategy { suspend fun collectIterationFiles(project: Project): List { val ctx = currentCoroutineContext() val job = ctx.job + val projectState = project.service().state - val indexTargets = if (false) { // (projectState.isProject) { + val indexTargets = if (projectState.isProject) { listOf(ProjectFileIndex.getInstance(project) to project.name) } else { val moduleManager = ModuleManager.getInstance(project) diff --git a/src/main/kotlin/com/mituuz/fuzzier/ui/preview/CoroutinePreviewAlarmProvider.kt b/src/main/kotlin/com/mituuz/fuzzier/ui/preview/CoroutinePreviewAlarmProvider.kt index b25fa7c1..cf595afd 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/ui/preview/CoroutinePreviewAlarmProvider.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/ui/preview/CoroutinePreviewAlarmProvider.kt @@ -49,9 +49,11 @@ class CoroutinePreviewAlarmProvider(val actionScope: CoroutineScope?) : PreviewA previewJob?.cancel() previewJob = actionScope?.launch(Dispatchers.Default) { - val file = withContext(Dispatchers.IO) { - VirtualFileManager.getInstance().findFileByUrl(fileUrl) - } + val file = + selectedValue.virtualFile + ?: withContext(Dispatchers.IO) { + VirtualFileManager.getInstance().findFileByUrl(fileUrl) + } if (file == null) return@launch From a5cce1fb95311b5cd4bec073bbec1997f08c9dcd Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 14:24:10 +0200 Subject: [PATCH 12/31] First working iteration --- .../com/mituuz/fuzzier/entities/GrepConfig.kt | 4 +- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 4 +- .../mituuz/fuzzier/grep/FuzzyGrepVariants.kt | 10 +- .../fuzzier/grep/backend/BackendResolver.kt | 20 +- .../fuzzier/grep/backend/BackendStrategy.kt | 195 +++++++++--------- .../fuzzier/grep/backend/FuzzierGrep.kt | 45 ++-- 6 files changed, 134 insertions(+), 144 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/GrepConfig.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/GrepConfig.kt index 481910d5..d320632b 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/GrepConfig.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/GrepConfig.kt @@ -24,13 +24,15 @@ package com.mituuz.fuzzier.entities +import com.intellij.openapi.vfs.VirtualFile + enum class CaseMode { SENSITIVE, INSENSITIVE, } class GrepConfig( - val targets: List, + val targets: Set?, val caseMode: CaseMode, val title: String = "", val supportsSecondaryField: Boolean = true, diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index e270136e..e69a0de2 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -74,7 +74,7 @@ open class FuzzyGrep : FuzzyAction() { open fun getGrepConfig(project: Project): GrepConfig { return GrepConfig( - targets = listOf("."), + targets = null, caseMode = CaseMode.SENSITIVE, title = "Fuzzy Grep", ) @@ -194,7 +194,7 @@ open class FuzzyGrep : FuzzyAction() { private fun validVf(virtualFile: VirtualFile, project: Project): Boolean { val clm = ChangeListManager.getInstance(project) - return !clm.isIgnoredFile(virtualFile) + return !virtualFile.isDirectory && !clm.isIgnoredFile(virtualFile) && !virtualFile.fileType.isBinary } private fun createListeners(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepVariants.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepVariants.kt index f12788e2..89a28a11 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepVariants.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrepVariants.kt @@ -40,7 +40,7 @@ class FuzzyGrepOpenTabsCI : FuzzyGrep() { override fun getGrepConfig(project: Project): GrepConfig { val fileEditorManager = FileEditorManager.getInstance(project) val openFiles: Array = fileEditorManager.openFiles - val targets = openFiles.map { it.path } + val targets = openFiles.toSet() return GrepConfig( targets = targets, @@ -54,7 +54,7 @@ class FuzzyGrepOpenTabs : FuzzyGrep() { override fun getGrepConfig(project: Project): GrepConfig { val fileEditorManager = FileEditorManager.getInstance(project) val openFiles: Array = fileEditorManager.openFiles - val targets = openFiles.map { it.path } + val targets = openFiles.toSet() return GrepConfig( targets = targets, @@ -69,7 +69,7 @@ class FuzzyGrepCurrentBufferCI : FuzzyGrep() { val editor = FileEditorManager.getInstance(project).selectedTextEditor val virtualFile: VirtualFile? = editor?.let { FileEditorManager.getInstance(project).selectedFiles.firstOrNull() } - val targets = virtualFile?.path?.let { listOf(it) } ?: emptyList() + val targets = virtualFile?.let { setOf(it) } ?: emptySet() return GrepConfig( targets = targets, @@ -85,7 +85,7 @@ class FuzzyGrepCurrentBuffer : FuzzyGrep() { val editor = FileEditorManager.getInstance(project).selectedTextEditor val virtualFile: VirtualFile? = editor?.let { FileEditorManager.getInstance(project).selectedFiles.firstOrNull() } - val targets = virtualFile?.path?.let { listOf(it) } ?: emptyList() + val targets = virtualFile?.let { setOf(it) } ?: emptySet() return GrepConfig( targets = targets, @@ -99,7 +99,7 @@ class FuzzyGrepCurrentBuffer : FuzzyGrep() { class FuzzyGrepCI : FuzzyGrep() { override fun getGrepConfig(project: Project): GrepConfig { return GrepConfig( - targets = listOf("."), + targets = null, caseMode = CaseMode.INSENSITIVE, title = FuzzyGrepTitles.DEFAULT, ) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt index beec49f3..0c161828 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt @@ -30,16 +30,16 @@ class BackendResolver(val isWindows: Boolean) { suspend fun resolveBackend(commandRunner: CommandRunner, projectBasePath: String): Result { return when { true -> Result.success(FuzzierGrep) - isInstalled(commandRunner, "rg", projectBasePath) -> Result.success(BackendStrategy.Ripgrep) - isWindows && isInstalled( - commandRunner, - "findstr", - projectBasePath - ) -> Result.success(BackendStrategy.Findstr) - - !isWindows && isInstalled(commandRunner, "grep", projectBasePath) -> Result.success( - BackendStrategy.Grep - ) +// isInstalled(commandRunner, "rg", projectBasePath) -> Result.success(BackendStrategy.Ripgrep) +// isWindows && isInstalled( +// commandRunner, +// "findstr", +// projectBasePath +// ) -> Result.success(BackendStrategy.Findstr) +// +// !isWindows && isInstalled(commandRunner, "grep", projectBasePath) -> Result.success( +// BackendStrategy.Grep +// ) else -> Result.failure(Exception("No suitable grep command found")) } diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 81ea4627..94b4c3ac 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -26,7 +26,6 @@ package com.mituuz.fuzzier.grep.backend import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.CaseMode import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer @@ -57,103 +56,103 @@ sealed interface BackendStrategy { fun supportsSecondaryField(): Boolean = false - object Ripgrep : BackendStrategy { - override val name = "ripgrep" - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List { - val commands = mutableListOf("rg") - - if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - commands.add("--smart-case") - commands.add("-F") - } - - commands.addAll( - mutableListOf( - "--no-heading", - "--color=never", - "-n", - "--with-filename", - "--column" - ) - ) - secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> - val glob = "*.${ext}" - commands.addAll(listOf("-g", glob)) - } - commands.add(searchString) - commands.addAll(grepConfig.targets) - return commands - } - - override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { - val line = line.replace(projectBasePath, ".") - return RowContainer.rgRowContainerFromString(line, projectBasePath) - } - - override fun supportsSecondaryField(): Boolean { - return true - } - } - - object Findstr : BackendStrategy { - override val name = "findstr" - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List { - val commands = mutableListOf("findstr") - - if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - commands.add("/I") - } - - commands.addAll( - mutableListOf( - "/p", - "/s", - "/n", - "/C:$searchString" - ) - ) - commands.addAll(grepConfig.targets.map { if (it == ".") "*" else it }) - return commands - } - } - - object Grep : BackendStrategy { - override val name = "grep" - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List { - val commands = mutableListOf("grep") - - if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - commands.add("-i") - } - - commands.addAll( - mutableListOf( - "--color=none", - "-r", - "-H", - "-n", - searchString - ) - ) - commands.addAll(grepConfig.targets) - return commands - } - } +// object Ripgrep : BackendStrategy { +// override val name = "ripgrep" +// +// override fun buildCommand( +// grepConfig: GrepConfig, +// searchString: String, +// secondarySearchString: String? +// ): List { +// val commands = mutableListOf("rg") +// +// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { +// commands.add("--smart-case") +// commands.add("-F") +// } +// +// commands.addAll( +// mutableListOf( +// "--no-heading", +// "--color=never", +// "-n", +// "--with-filename", +// "--column" +// ) +// ) +// secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> +// val glob = "*.${ext}" +// commands.addAll(listOf("-g", glob)) +// } +// commands.add(searchString) +// commands.addAll(grepConfig.targets) +// return commands +// } +// +// override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { +// val line = line.replace(projectBasePath, ".") +// return RowContainer.rgRowContainerFromString(line, projectBasePath) +// } +// +// override fun supportsSecondaryField(): Boolean { +// return true +// } +// } +// +// object Findstr : BackendStrategy { +// override val name = "findstr" +// +// override fun buildCommand( +// grepConfig: GrepConfig, +// searchString: String, +// secondarySearchString: String? +// ): List { +// val commands = mutableListOf("findstr") +// +// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { +// commands.add("/I") +// } +// +// commands.addAll( +// mutableListOf( +// "/p", +// "/s", +// "/n", +// "/C:$searchString" +// ) +// ) +// commands.addAll(grepConfig.targets.map { if (it == ".") "*" else it }) +// return commands +// } +// } +// +// object Grep : BackendStrategy { +// override val name = "grep" +// +// override fun buildCommand( +// grepConfig: GrepConfig, +// searchString: String, +// secondarySearchString: String? +// ): List { +// val commands = mutableListOf("grep") +// +// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { +// commands.add("-i") +// } +// +// commands.addAll( +// mutableListOf( +// "--color=none", +// "-r", +// "-H", +// "-n", +// searchString +// ) +// ) +// commands.addAll(grepConfig.targets) +// return commands +// } +// } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 03e8695c..a45ad619 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -31,7 +31,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.rootManager import com.intellij.openapi.roots.FileIndex import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.vcs.changes.ChangeListManager import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope @@ -71,7 +70,7 @@ object FuzzierGrep : BackendStrategy { val fileCollectionStart = System.nanoTime() - val files = collectFiles(searchString, fileFilter, project, grepConfig) + val files = grepConfig.targets ?: collectFiles(searchString, fileFilter, project, grepConfig) val fileCollectionEnd = System.nanoTime() val fileCollectionDuration = (fileCollectionEnd - fileCollectionStart) / 1_000_000.0 @@ -183,35 +182,25 @@ object FuzzierGrep : BackendStrategy { } } } else { - // Case A: Short String (Length < 3) - Linear iteration - files.addAll(collectIterationFiles(project)) - } - - return files - } - - suspend fun collectIterationFiles(project: Project): List { - val ctx = currentCoroutineContext() - val job = ctx.job - val projectState = project.service().state + val ctx = currentCoroutineContext() + val job = ctx.job + val projectState = project.service().state + + 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 } + } - 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 collectFiles( + targets = indexTargets, + shouldContinue = { job.isActive }, + fileFilter = fileFilter + ) } - return collectFiles( - targets = indexTargets, - shouldContinue = { job.isActive }, - fileFilter = buildFileFilter(project) - ) - } - - private fun buildFileFilter(project: Project): (VirtualFile) -> Boolean { - val clm = ChangeListManager.getInstance(project) - return { vf -> !vf.isDirectory && !clm.isIgnoredFile(vf) && !vf.fileType.isBinary } + return files } private fun collectFiles( From f192e64e04146a2c8893957bde0b168bbc129e9b Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 14:32:05 +0200 Subject: [PATCH 13/31] Add support for secondary field --- .../kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt | 17 ++++++++++++++--- .../mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index e69a0de2..45831824 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -186,15 +186,26 @@ open class FuzzyGrep : FuzzyAction() { listModel, projectBasePath, project - ) { vf -> validVf(vf, project) } + ) { vf -> validVf(vf, project, secondaryFieldText) } } return listModel } - private fun validVf(virtualFile: VirtualFile, project: Project): Boolean { + private fun validVf(virtualFile: VirtualFile, project: Project, secondaryFieldText: String? = null): Boolean { + if (virtualFile.isDirectory) return false + if (virtualFile.fileType.isBinary) return false + val clm = ChangeListManager.getInstance(project) - return !virtualFile.isDirectory && !clm.isIgnoredFile(virtualFile) && !virtualFile.fileType.isBinary + if (clm.isIgnoredFile(virtualFile)) return false + + if (secondaryFieldText.isNullOrBlank()) { + return true + } else if (virtualFile.extension.equals(secondaryFieldText, ignoreCase = true)) { + return true + } + + return false } private fun createListeners(project: Project) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index a45ad619..9c31e60d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -49,6 +49,9 @@ import javax.swing.DefaultListModel object FuzzierGrep : BackendStrategy { override val name = "fuzzier" private val fuzzierUtil = FuzzierUtil() + override fun supportsSecondaryField(): Boolean { + return true + } override fun buildCommand( grepConfig: GrepConfig, From 54a4a002d542ed4c762dc4d80c096edbc28e30fe Mon Sep 17 00:00:00 2001 From: Mitja Date: Wed, 24 Dec 2025 14:33:51 +0200 Subject: [PATCH 14/31] Only fetch clm once --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 45831824..ecd0c566 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -104,8 +104,7 @@ open class FuzzyGrep : FuzzyAction() { defaultDoc = EditorFactory.getInstance().createDocument("") val showSecondaryField = backend!!.supportsSecondaryField() && grepConfig.supportsSecondaryField component = FuzzyFinderComponent( - project = project, - showSecondaryField = showSecondaryField + project = project, showSecondaryField = showSecondaryField ) previewAlarmProvider = CoroutinePreviewAlarmProvider(actionScope) previewAlarm = previewAlarmProvider?.getPreviewAlarm(component, defaultDoc) @@ -157,8 +156,7 @@ open class FuzzyGrep : FuzzyAction() { try { val results = withContext(Dispatchers.IO) { findInFiles( - searchString, - project + searchString, project ) } coroutineContext.ensureActive() @@ -179,24 +177,19 @@ open class FuzzyGrep : FuzzyAction() { if (backend != null && projectBasePath != null) { val secondaryFieldText = (component as FuzzyFinderComponent).getSecondaryText() backend!!.handleSearch( - grepConfig, - searchString, - secondaryFieldText, - commandRunner, - listModel, - projectBasePath, - project - ) { vf -> validVf(vf, project, secondaryFieldText) } + grepConfig, searchString, secondaryFieldText, commandRunner, listModel, projectBasePath, project + ) { vf -> validVf(vf, secondaryFieldText, ChangeListManager.getInstance(project)) } } return listModel } - private fun validVf(virtualFile: VirtualFile, project: Project, secondaryFieldText: String? = null): Boolean { + private fun validVf( + virtualFile: VirtualFile, secondaryFieldText: String? = null, clm: ChangeListManager + ): Boolean { if (virtualFile.isDirectory) return false if (virtualFile.fileType.isBinary) return false - val clm = ChangeListManager.getInstance(project) if (clm.isIgnoredFile(virtualFile)) return false if (secondaryFieldText.isNullOrBlank()) { @@ -224,15 +217,12 @@ open class FuzzyGrep : FuzzyAction() { private fun handleInput(project: Project) { val selectedValue = component.fileList.selectedValue - val virtualFile = - VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") + val virtualFile = VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") virtualFile?.let { val fileEditorManager = FileEditorManager.getInstance(project) FileOpeningUtil.openFile( - fileEditorManager, - virtualFile, - globalState.newTab + fileEditorManager, virtualFile, globalState.newTab ) { popup.cancel() ApplicationManager.getApplication().invokeLater { From 9e87975d80fd4e84d72c55c8ab3d4fc159aa1a7c Mon Sep 17 00:00:00 2001 From: Mitja Date: Thu, 25 Dec 2025 11:25:46 +0200 Subject: [PATCH 15/31] Remove initial plan --- fuzzier-grep.md | 53 ------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 fuzzier-grep.md diff --git a/fuzzier-grep.md b/fuzzier-grep.md deleted file mode 100644 index 3af51421..00000000 --- a/fuzzier-grep.md +++ /dev/null @@ -1,53 +0,0 @@ -# Pure Kotlin Grep Backend Plan (The "Fuzzier" Strategy) - -## 1. Core APIs to Leverage - -- **`CacheManager.getInstance(project).getFilesWithWord()`**: Find files indexed with specific words. -- **`ChangeListManager.getInstance(project)`**: Check if a file is ignored by VCS (e.g., via `.gitignore`). -- **`GlobalSearchScope`**: Restrict searches to the project scope, which naturally excludes library sources and excluded - folders. -- **`ReadAction.nonBlocking`**: Ensures thread-safe index access that cancels automatically on user input. - -## 2. The Hybrid Search Strategy - -### Case A: Short String (Length < 3) - -* **Action**: Use a linear iteration (like your `IntelliJIterationFileCollector`). -* **VCS Check**: For every file encountered, check `ChangeListManager.isIgnoredFile(vf)`. Skip if `true`. - -### Case B: Long String (Length >= 3) - -* **Action**: Use `CacheManager.getFilesWithWord()`. -* **VCS Check**: - -1. The `GlobalSearchScope.projectScope(project)` already excludes many non-project files. -2. Filter the resulting `VirtualFile[]` against `ChangeListManager.isIgnoredFile(vf)`. - -## 3. Passing the VCS Filter - -To make this work cleanly, we should update `GrepConfig` or the `handleSearch` signature to include a file filter. This -allows `FuzzierVCS` (or any other action) to tell the backend exactly what to ignore. - -1. **Define a Filter**: In `FuzzyGrep`, create a `fileFilter: (VirtualFile) -> Boolean`. -2. **VCS Logic**: Inside the filter, use `!ChangeListManager.getInstance(project).isIgnoredFile(vf)`. -3. **Backend Execution**: The `Fuzzier` backend should apply this filter before reading file contents. - -## 4. Implementation Steps for `Fuzzier.handleSearch` - -1. **Get Project instance**: Since `BackendStrategy` doesn't have it, we should add `project: Project` to the - `handleSearch` parameters. -2. **Check Length**: - -- If `searchString.length < 3`: Manually iterate project files using the VCS-aware filter. -- If `>= 3`: Query `CacheManager` using `GlobalSearchScope.projectScope(project)`, then filter results by VCS status and - `grepConfig.targets`. - -3. **Read & Match**: For valid files, load text, find matches, and batch-add to `listModel`. -4. **Batch & Cancel**: Use `withContext(Dispatchers.Main)` for batching and `ensureActive()` for cancellation. -5. **Limit**: Stop at 1000 results. - -## 5. Performance Tips - -- **Scope is Key**: `GlobalSearchScope` is very powerful; it can be restricted to specific modules or directories, which - maps perfectly to your `grepConfig.targets`. -- **Read Action**: Always perform `CacheManager` and `ChangeListManager` checks inside a `ReadAction`. From 3b574a084ca9fc2df4777322841b9bb33f566e04 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sat, 10 Jan 2026 15:35:55 +0200 Subject: [PATCH 16/31] Only use the first known complete word for search optimization --- .../com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 9c31e60d..6dec316c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -162,15 +162,16 @@ object FuzzierGrep : BackendStrategy { val files = mutableListOf() val trimmedSearch = searchString.trim() - if (trimmedSearch.contains(" ")) { - // Case B: Long String (Length >= 3) - PsiSearchHelper - val searchString = trimmedSearch.substringBeforeLast(" ") + if (trimmedSearch.count { it == ' ' } >= 2) { + // Extract the first complete word - the only one we can be sure is a full word + val words = trimmedSearch.split(" ") + val firstCompleteWord = words[1] - if (searchString.isNotEmpty()) { + if (firstCompleteWord.isNotEmpty()) { ReadAction.run { val helper = PsiSearchHelper.getInstance(project) helper.processAllFilesWithWord( - searchString, + firstCompleteWord, GlobalSearchScope.projectScope(project), Processor { psiFile -> psiFile.virtualFile?.let { vf -> From fb6a0ad0befc0770f03db41b938667e9f23083b6 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 11 Jan 2026 11:17:19 +0200 Subject: [PATCH 17/31] Add setting to force fuzzier as grep backend --- .../FuzzierGlobalSettingsComponent.kt | 36 ++++- .../fuzzier/components/SettingsComponent.kt | 5 + .../fuzzier/grep/backend/BackendResolver.kt | 23 ++- .../fuzzier/grep/backend/BackendStrategy.kt | 143 ++++++------------ .../FuzzierGlobalSettingsConfigurable.kt | 5 + .../settings/FuzzierGlobalSettingsService.kt | 7 + .../fuzzier/search/BackendResolverTest.kt | 104 +++---------- .../fuzzier/search/BackendStrategyTest.kt | 126 ++------------- .../FuzzierGlobalSettingsConfigurableTest.kt | 40 +++++ 9 files changed, 178 insertions(+), 311 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt index a9376ca1..593355ff 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzierGlobalSettingsComponent.kt @@ -85,16 +85,26 @@ class FuzzierGlobalSettingsComponent( JBCheckBox(), "Fuzzy Grep: Preview entire file", """ Toggles showing the full file in the preview.

- + If set, preview will use full syntax highlighting.
Otherwise, preview will only use limited syntax highlighting and show a slice around the match.

- - Disabling this option may improve performance on very large files, + + Disabling this option may improve performance on very large files, for small-to-medium files the performance impact is negligible. """.trimIndent(), false ) + val grepBackendSelector = SettingsComponent( + ComboBox(), "Grep backend", + """ + Select which backend to use for Fuzzy Grep.

+ Dynamic: Uses ripgrep (rg) if available, otherwise falls back to Fuzzier.
+ Fuzzier: Uses the built-in Fuzzier backend. + """.trimIndent(), + false + ) + val globalExclusionTextArea: JBTextArea = JBTextArea().apply { rows = 5 lineWrap = true @@ -328,6 +338,7 @@ class FuzzierGlobalSettingsComponent( .addComponent(debounceTimerValue) .addComponent(fileListLimit) .addComponent(fuzzyGrepShowFullFile) + .addComponent(grepBackendSelector) .addComponent(globalExclusionSet) .addSeparator() @@ -430,6 +441,25 @@ class FuzzierGlobalSettingsComponent( popupSizingSelector.getPopupSizingComboBox().addItem(s) } + grepBackendSelector.getGrepBackendComboBox().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 backend = value as GrepBackend + renderer.text = backend.text + return renderer + } + } + for (backend in GrepBackend.entries) { + grepBackendSelector.getGrepBackendComboBox().addItem(backend) + } + 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 594e4367..b7065487 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/SettingsComponent.kt @@ -104,6 +104,11 @@ class SettingsComponent { return component as ComboBox } + fun getGrepBackendComboBox(): 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/grep/backend/BackendResolver.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt index 0c161828..c7a5a17d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendResolver.kt @@ -24,24 +24,21 @@ package com.mituuz.fuzzier.grep.backend +import com.intellij.openapi.components.service import com.mituuz.fuzzier.runner.CommandRunner +import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService +import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService.GrepBackend class BackendResolver(val isWindows: Boolean) { suspend fun resolveBackend(commandRunner: CommandRunner, projectBasePath: String): Result { - return when { - true -> Result.success(FuzzierGrep) -// isInstalled(commandRunner, "rg", projectBasePath) -> Result.success(BackendStrategy.Ripgrep) -// isWindows && isInstalled( -// commandRunner, -// "findstr", -// projectBasePath -// ) -> Result.success(BackendStrategy.Findstr) -// -// !isWindows && isInstalled(commandRunner, "grep", projectBasePath) -> Result.success( -// BackendStrategy.Grep -// ) + val grepBackendSetting = service().state.grepBackend - else -> Result.failure(Exception("No suitable grep command found")) + return when (grepBackendSetting) { + GrepBackend.FUZZIER -> Result.success(FuzzierGrep) + GrepBackend.DYNAMIC -> when { + isInstalled(commandRunner, "rg", projectBasePath) -> Result.success(Ripgrep) + else -> Result.success(FuzzierGrep) + } } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 94b4c3ac..9911cb2d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -26,6 +26,7 @@ package com.mituuz.fuzzier.grep.backend import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.mituuz.fuzzier.entities.CaseMode import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer @@ -55,104 +56,50 @@ sealed interface BackendStrategy { } fun supportsSecondaryField(): Boolean = false +} -// object Ripgrep : BackendStrategy { -// override val name = "ripgrep" -// -// override fun buildCommand( -// grepConfig: GrepConfig, -// searchString: String, -// secondarySearchString: String? -// ): List { -// val commands = mutableListOf("rg") -// -// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { -// commands.add("--smart-case") -// commands.add("-F") -// } -// -// commands.addAll( -// mutableListOf( -// "--no-heading", -// "--color=never", -// "-n", -// "--with-filename", -// "--column" -// ) -// ) -// secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> -// val glob = "*.${ext}" -// commands.addAll(listOf("-g", glob)) -// } -// commands.add(searchString) -// commands.addAll(grepConfig.targets) -// return commands -// } -// -// override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { -// val line = line.replace(projectBasePath, ".") -// return RowContainer.rgRowContainerFromString(line, projectBasePath) -// } -// -// override fun supportsSecondaryField(): Boolean { -// return true -// } -// } -// -// object Findstr : BackendStrategy { -// override val name = "findstr" -// -// override fun buildCommand( -// grepConfig: GrepConfig, -// searchString: String, -// secondarySearchString: String? -// ): List { -// val commands = mutableListOf("findstr") -// -// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { -// commands.add("/I") -// } -// -// commands.addAll( -// mutableListOf( -// "/p", -// "/s", -// "/n", -// "/C:$searchString" -// ) -// ) -// commands.addAll(grepConfig.targets.map { if (it == ".") "*" else it }) -// return commands -// } -// } -// -// object Grep : BackendStrategy { -// override val name = "grep" -// -// override fun buildCommand( -// grepConfig: GrepConfig, -// searchString: String, -// secondarySearchString: String? -// ): List { -// val commands = mutableListOf("grep") -// -// if (grepConfig.caseMode == CaseMode.INSENSITIVE) { -// commands.add("-i") -// } -// -// commands.addAll( -// mutableListOf( -// "--color=none", -// "-r", -// "-H", -// "-n", -// searchString -// ) -// ) -// commands.addAll(grepConfig.targets) -// return commands -// } -// } +object Ripgrep : BackendStrategy { + override val name = "ripgrep" -} + override fun buildCommand( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String? + ): List { + val commands = mutableListOf("rg") + if (grepConfig.caseMode == CaseMode.INSENSITIVE) { + commands.add("--smart-case") + commands.add("-F") + } + + commands.addAll( + mutableListOf( + "--no-heading", + "--color=never", + "-n", + "--with-filename", + "--column" + ) + ) + secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> + val glob = "*.${ext}" + commands.addAll(listOf("-g", glob)) + } + commands.add(searchString) + + // Convert VirtualFiles to paths, or use "." if targets is null + val targetPaths = grepConfig.targets?.map { it.path } ?: listOf(".") + commands.addAll(targetPaths) + return commands + } + + override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { + val line = line.replace(projectBasePath, ".") + return RowContainer.rgRowContainerFromString(line, projectBasePath) + } + + override fun supportsSecondaryField(): Boolean { + return true + } +} diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt index bcdceec0..25b42249 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsConfigurable.kt @@ -68,6 +68,7 @@ class FuzzierGlobalSettingsConfigurable : Configurable { component.previewFontSize.getIntSpinner().value = state.previewFontSize component.fileListSpacing.getIntSpinner().value = state.fileListSpacing component.fuzzyGrepShowFullFile.getCheckBox().isSelected = state.fuzzyGrepShowFullFile + component.grepBackendSelector.getGrepBackendComboBox().selectedIndex = state.grepBackend.ordinal // Hide dimension settings when Auto size is selected updateDimensionVisibility(state.popupSizing) @@ -129,6 +130,7 @@ class FuzzierGlobalSettingsConfigurable : Configurable { || state.matchWeightStreakModifier != component.matchWeightStreakModifier.getIntSpinner().value || state.matchWeightFilename != component.matchWeightFilename.getIntSpinner().value || state.globalExclusionSet != newGlobalSet + || state.grepBackend != component.grepBackendSelector.getGrepBackendComboBox().selectedItem } override fun apply() { @@ -181,6 +183,9 @@ class FuzzierGlobalSettingsConfigurable : Configurable { .filter { it.isNotEmpty() } .toSet() state.globalExclusionSet = newGlobalSet + + state.grepBackend = + FuzzierGlobalSettingsService.GrepBackend.entries.toTypedArray()[component.grepBackendSelector.getGrepBackendComboBox().selectedIndex] } override fun disposeUIResources() { diff --git a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt index c8d08862..e8fac6d5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/settings/FuzzierGlobalSettingsService.kt @@ -70,6 +70,8 @@ class FuzzierGlobalSettingsService : PersistentStateComponent Date: Sun, 11 Jan 2026 16:13:57 +0200 Subject: [PATCH 18/31] Increment version and add change notes --- build.gradle.kts | 74 ++++-------------------------------------------- changelog.md | 7 +++++ 2 files changed, 12 insertions(+), 69 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a91f4df4..2c9fd800 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 = "2.0.1" +val currentVersion = "2.1.0" val myGroup = "com.mituuz" version = currentVersion group = myGroup @@ -40,78 +40,14 @@ intellijPlatform { changeNotes = """

Version $currentVersion

    -
  • Fix incorrect grep command
  • -
  • Partially fix findstr command
  • -
  • Re-add the backend name to the popup title
  • -
-

Known issues

-
    -
  • findstr does not work with currently open tabs -
      -
    • To reduce the maintenance burden, I may remove support later
    • -
    • Performance is poor enough that I thought that the command wasn't returning any results
    • -
    -
  • -
-

Version 2.0.0

-

This version contains larger refactors and multiple new actions enabled by them.

-

I'm updating the existing package structure to keep things nicer and not supporting the old actions to avoid possible problems in the future.

- -

Breaking changes

-

Rename existing actions

-
    -
  • com.mituuz.fuzzier.FuzzyGrepCaseInsensitive to com.mituuz.fuzzier.grep.FuzzyGrepCI
  • -
  • com.mituuz.fuzzier.FuzzyGrep to com.mituuz.fuzzier.grep.FuzzyGrep
  • -
  • com.mituuz.fuzzier.Fuzzier to com.mituuz.fuzzier.search.Fuzzier
  • -
  • com.mituuz.fuzzier.FuzzierVCS to com.mituuz.fuzzier.search.FuzzierVCS
  • -
  • com.mituuz.fuzzier.FuzzyMover to com.mituuz.fuzzier.operation.FuzzyMover
  • -
-

Update default list movement keys

-
    -
  • From CTRL + j and CTRL + k to CTRL + n and CTRL + p
  • -
- -

New actions

-

Added some new grep variations

-
com.mituuz.fuzzier.grep.FuzzyGrepOpenTabsCI
-    com.mituuz.fuzzier.grep.FuzzyGrepOpenTabs
-    com.mituuz.fuzzier.grep.FuzzyGrepCurrentBufferCI
-    com.mituuz.fuzzier.grep.FuzzyGrepCurrentBuffer
- -

Example mappings

-
" File search
-    nmap <Leader>sf <action>(com.mituuz.fuzzier.search.Fuzzier)
-    nmap <Leader>sg <action>(com.mituuz.fuzzier.search.FuzzierVCS)
-    
-    " Mover
-    nmap <Leader>fm <action>(com.mituuz.fuzzier.operation.FuzzyMover)
-    
-    " Grepping
-    nmap <Leader>ss <action>(com.mituuz.fuzzier.grep.FuzzyGrepCI)
-    nmap <Leader>sS <action>(com.mituuz.fuzzier.grep.FuzzyGrep)
-    nmap <Leader>st <action>(com.mituuz.fuzzier.grep.FuzzyGrepOpenTabsCI)
-    nmap <Leader>sT <action>(com.mituuz.fuzzier.grep.FuzzyGrepOpenTabs)
-    nmap <Leader>sb <action>(com.mituuz.fuzzier.grep.FuzzyGrepCurrentBufferCI)
-    nmap <Leader>sB <action>(com.mituuz.fuzzier.grep.FuzzyGrepCurrentBuffer)
- -

New features

-
    -
  • Popup now defaults to auto-sized, which scales with the current window
  • -
  • You can revert this from the settings
  • -
- -

Other changes

-
    -
  • Refactor file search to use coroutines +
  • Add built-in Fuzzier grep backend
      -
    • Handle list size limiting during processing instead of doing them separately
    • +
    • Replaces grep and findstr fallback implementations
    • +
    • Add setting to choose between Dynamic (uses rg if available, otherwise Fuzzier) and Fuzzier backends
  • -
  • Add debouncing for file preview using SingleAlarm
  • -
  • Refactor everything
  • -
  • Remove manual handling of the divider location (use JBSplitter instead) and unify styling
  • +
  • Migrate from Timer to coroutines for debouncing
- """.trimIndent() ideaVersion { diff --git a/changelog.md b/changelog.md index 1ba754e5..5ae1d000 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## Version 2.1.0 + +- Add a built-in Fuzzier grep backend + - Replaces `grep` and `findstr` fallback implementations + - Add setting to choose between Dynamic (uses `rg` if available, otherwise Fuzzier) and Fuzzier backends +- Migrate from `Timer` to coroutines for debouncing + ## Version 2.0.1 - Fix incorrect `grep` command From a64ecca65a34c752977547c8965f98b48279a97a Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 14:13:20 +0200 Subject: [PATCH 19/31] Add change notes for 157 --- changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 5ae1d000..9e14a29e 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,8 @@ ## Version 2.1.0 +- Add a fallback solution for file path handling on Windows + - Thanks to [s0ders](https://github.com/s0ders)! - Add a built-in Fuzzier grep backend - Replaces `grep` and `findstr` fallback implementations - Add setting to choose between Dynamic (uses `rg` if available, otherwise Fuzzier) and Fuzzier backends From 481a74fcc2ab428e257656b609342c28fb705251 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 15:39:40 +0200 Subject: [PATCH 20/31] Remove perf printing --- .../mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 6dec316c..d037f4b4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -71,22 +71,12 @@ object FuzzierGrep : BackendStrategy { ) { if (project == null) return - val fileCollectionStart = System.nanoTime() - val files = grepConfig.targets ?: collectFiles(searchString, fileFilter, project, grepConfig) - val fileCollectionEnd = System.nanoTime() - val fileCollectionDuration = (fileCollectionEnd - fileCollectionStart) / 1_000_000.0 - println("File collection took $fileCollectionDuration ms") - - println("Searching from ${files.size} files with ${grepConfig.caseMode} case mode and $searchString") - var count = 0 val batchSize = 20 val currentBatch = mutableListOf() - val fileProcessingStart = System.nanoTime() - for (file in files) { currentCoroutineContext().ensureActive() @@ -141,11 +131,6 @@ object FuzzierGrep : BackendStrategy { if (count >= 1000) break } - val fileProcessingEnd = System.nanoTime() - - val fileProcessingDuration = (fileProcessingEnd - fileProcessingStart) / 1_000_000.0 - println("File processing took $fileProcessingDuration ms") - if (currentBatch.isNotEmpty()) { withContext(Dispatchers.Main) { listModel.addAll(currentBatch) From 4a87684448080658adb3a90148eaff907ce3ced3 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 15:47:32 +0200 Subject: [PATCH 21/31] Fix array check and use max size from settings --- .../fuzzier/grep/backend/FuzzierGrep.kt | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index d037f4b4..25de59fc 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -35,12 +35,14 @@ import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.PsiSearchHelper +import com.intellij.openapi.components.service import com.intellij.util.Processor import com.mituuz.fuzzier.entities.CaseMode import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer import com.mituuz.fuzzier.runner.CommandRunner +import com.mituuz.fuzzier.settings.FuzzierGlobalSettingsService import com.mituuz.fuzzier.settings.FuzzierSettingsService import com.mituuz.fuzzier.util.FuzzierUtil import kotlinx.coroutines.* @@ -76,6 +78,7 @@ object FuzzierGrep : BackendStrategy { var count = 0 val batchSize = 20 val currentBatch = mutableListOf() + val maxResults = service().state.fileListLimit for (file in files) { currentCoroutineContext().ensureActive() @@ -125,10 +128,10 @@ object FuzzierGrep : BackendStrategy { } } - if (count >= 1000) break + if (count >= maxResults) break } - if (count >= 1000) break + if (count >= maxResults) break } if (currentBatch.isNotEmpty()) { @@ -146,29 +149,27 @@ object FuzzierGrep : BackendStrategy { ): List { val files = mutableListOf() val trimmedSearch = searchString.trim() + val words = trimmedSearch.split(" ") - if (trimmedSearch.count { it == ' ' } >= 2) { + if (words.size > 1 && words[1].isNotEmpty() && trimmedSearch.count { it == ' ' } >= 2) { // Extract the first complete word - the only one we can be sure is a full word - val words = trimmedSearch.split(" ") val firstCompleteWord = words[1] - if (firstCompleteWord.isNotEmpty()) { - ReadAction.run { - val helper = PsiSearchHelper.getInstance(project) - helper.processAllFilesWithWord( - firstCompleteWord, - GlobalSearchScope.projectScope(project), - Processor { psiFile -> - psiFile.virtualFile?.let { vf -> - if (fileFilter(vf)) { - files.add(vf) - } + ReadAction.run { + val helper = PsiSearchHelper.getInstance(project) + helper.processAllFilesWithWord( + firstCompleteWord, + GlobalSearchScope.projectScope(project), + Processor { psiFile -> + psiFile.virtualFile?.let { vf -> + if (fileFilter(vf)) { + files.add(vf) } - true - }, - grepConfig.caseMode == CaseMode.SENSITIVE - ) - } + } + true + }, + grepConfig.caseMode == CaseMode.SENSITIVE + ) } } else { val ctx = currentCoroutineContext() From 2879c11f209c9c452f0309db0287b734b14be675 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 15:49:47 +0200 Subject: [PATCH 22/31] Handle file read failures --- .../fuzzier/grep/backend/FuzzierGrep.kt | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 25de59fc..bf5a836c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -84,34 +84,39 @@ object FuzzierGrep : BackendStrategy { currentCoroutineContext().ensureActive() val matches = withContext(Dispatchers.IO) { - val content = VfsUtil.loadText(file) - val lines = content.lines() - val fileMatches = mutableListOf() - - for ((index, line) in lines.withIndex()) { - currentCoroutineContext().ensureActive() - - val found = if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - line.contains(searchString, ignoreCase = true) - } else { - line.contains(searchString) - } + try { + val content = VfsUtil.loadText(file) + val lines = content.lines() + val fileMatches = mutableListOf() + + for ((index, line) in lines.withIndex()) { + currentCoroutineContext().ensureActive() + + val found = if (grepConfig.caseMode == CaseMode.INSENSITIVE) { + line.contains(searchString, ignoreCase = true) + } else { + line.contains(searchString) + } - if (found) { - val (filePath, basePath) = fuzzierUtil.extractModulePath(file.path, project) - fileMatches.add( - RowContainer( - filePath, - basePath, - file.name, - index, - line.trim(), - virtualFile = file + if (found) { + val (filePath, basePath) = fuzzierUtil.extractModulePath(file.path, project) + fileMatches.add( + RowContainer( + filePath, + basePath, + file.name, + index, + line.trim(), + virtualFile = file + ) ) - ) + } } + fileMatches + } catch (_: Exception) { + // Skip files that cannot be read (permissions, encoding issues, etc.) + emptyList() } - fileMatches } for (match in matches) { From f2dbd5fe3ac71851e8cac9541208b301522950fc Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 15:55:01 +0200 Subject: [PATCH 23/31] gs --- src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt | 3 ++- 1 file changed, 2 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 ecd0c566..3b9a138d 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -217,7 +217,8 @@ open class FuzzyGrep : FuzzyAction() { private fun handleInput(project: Project) { val selectedValue = component.fileList.selectedValue - val virtualFile = VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") + val virtualFile = selectedValue?.virtualFile + ?: VirtualFileManager.getInstance().findFileByUrl("file://${selectedValue?.getFileUri()}") virtualFile?.let { val fileEditorManager = FileEditorManager.getInstance(project) From 534c2b0e72c9655dd3798f52a3927d27ceba921d Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 15:57:10 +0200 Subject: [PATCH 24/31] Non-nullable parameter --- .../kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt | 2 +- .../kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 9911cb2d..ada68e19 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -48,7 +48,7 @@ sealed interface BackendStrategy { commandRunner: CommandRunner, listModel: DefaultListModel, projectBasePath: String, - project: Project? = null, + project: Project, fileFilter: (VirtualFile) -> Boolean = { true } ) { val commands = buildCommand(grepConfig, searchString, secondarySearchString) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index bf5a836c..4ce56e2a 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -68,11 +68,9 @@ object FuzzierGrep : BackendStrategy { commandRunner: CommandRunner, listModel: DefaultListModel, projectBasePath: String, - project: Project?, + project: Project, fileFilter: (VirtualFile) -> Boolean ) { - if (project == null) return - val files = grepConfig.targets ?: collectFiles(searchString, fileFilter, project, grepConfig) var count = 0 From 775101ed3fcee27dd601f80fa29a15c618c68aee Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 16:22:45 +0200 Subject: [PATCH 25/31] Update build changelog --- build.gradle.kts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2c9fd800..1b47f552 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,12 @@ intellijPlatform { changeNotes = """

Version $currentVersion

    -
  • Add built-in Fuzzier grep backend +
  • Add a fallback solution for file path handling on Windows + +
  • +
  • Add a built-in Fuzzier grep backend
    • Replaces grep and findstr fallback implementations
    • Add setting to choose between Dynamic (uses rg if available, otherwise Fuzzier) and Fuzzier backends
    • From 0d39517a8042f1eea6c2a18a50149ede0b681dd6 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 16:26:11 +0200 Subject: [PATCH 26/31] Separate ripgrep backend to a separate file --- .../fuzzier/grep/backend/BackendStrategy.kt | 47 ------------ .../mituuz/fuzzier/grep/backend/Ripgrep.kt | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index ada68e19..463743b5 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -26,7 +26,6 @@ package com.mituuz.fuzzier.grep.backend import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.mituuz.fuzzier.entities.CaseMode import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer @@ -57,49 +56,3 @@ sealed interface BackendStrategy { fun supportsSecondaryField(): Boolean = false } - -object Ripgrep : BackendStrategy { - override val name = "ripgrep" - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List { - val commands = mutableListOf("rg") - - if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - commands.add("--smart-case") - commands.add("-F") - } - - commands.addAll( - mutableListOf( - "--no-heading", - "--color=never", - "-n", - "--with-filename", - "--column" - ) - ) - secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> - val glob = "*.${ext}" - commands.addAll(listOf("-g", glob)) - } - commands.add(searchString) - - // Convert VirtualFiles to paths, or use "." if targets is null - val targetPaths = grepConfig.targets?.map { it.path } ?: listOf(".") - commands.addAll(targetPaths) - return commands - } - - override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { - val line = line.replace(projectBasePath, ".") - return RowContainer.rgRowContainerFromString(line, projectBasePath) - } - - override fun supportsSecondaryField(): Boolean { - return true - } -} diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt new file mode 100644 index 00000000..9e8d55d4 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.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.grep.backend + +import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.GrepConfig +import com.mituuz.fuzzier.entities.RowContainer + +object Ripgrep : BackendStrategy { + override val name = "ripgrep" + + override fun buildCommand( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String? + ): List { + val commands = mutableListOf("rg") + + if (grepConfig.caseMode == CaseMode.INSENSITIVE) { + commands.add("--smart-case") + commands.add("-F") + } + + commands.addAll( + mutableListOf( + "--no-heading", + "--color=never", + "-n", + "--with-filename", + "--column" + ) + ) + secondarySearchString?.removePrefix(".").takeIf { it?.isNotEmpty() == true }?.let { ext -> + val glob = "*.${ext}" + commands.addAll(listOf("-g", glob)) + } + commands.add(searchString) + + // Convert VirtualFiles to paths, or use "." if targets is null + val targetPaths = grepConfig.targets?.map { it.path } ?: listOf(".") + commands.addAll(targetPaths) + return commands + } + + override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { + val line = line.replace(projectBasePath, ".") + return RowContainer.rgRowContainerFromString(line, projectBasePath) + } + + override fun supportsSecondaryField(): Boolean { + return true + } +} From c5071fec38411735fa86aab47948ea7c3e71f8e3 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 16:37:46 +0200 Subject: [PATCH 27/31] Cleanup backend interface --- .../fuzzier/grep/backend/BackendStrategy.kt | 13 ++--------- .../fuzzier/grep/backend/FuzzierGrep.kt | 9 -------- .../mituuz/fuzzier/grep/backend/Ripgrep.kt | 23 +++++++++++++++---- .../mituuz/fuzzier/runner/CommandRunner.kt | 4 ++-- .../fuzzier/runner/DefaultCommandRunner.kt | 6 ++--- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index 463743b5..a2237133 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -28,17 +28,11 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig -import com.mituuz.fuzzier.entities.RowContainer import com.mituuz.fuzzier.runner.CommandRunner import javax.swing.DefaultListModel sealed interface BackendStrategy { val name: String - fun buildCommand(grepConfig: GrepConfig, searchString: String, secondarySearchString: String?): List - fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { - val line = line.replace(projectBasePath, ".") - return RowContainer.rowContainerFromString(line, projectBasePath) - } suspend fun handleSearch( grepConfig: GrepConfig, @@ -49,10 +43,7 @@ sealed interface BackendStrategy { projectBasePath: String, project: Project, fileFilter: (VirtualFile) -> Boolean = { true } - ) { - val commands = buildCommand(grepConfig, searchString, secondarySearchString) - commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, this) - } + ) - fun supportsSecondaryField(): Boolean = false + fun supportsSecondaryField(): Boolean = true } diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 4ce56e2a..0f7c8568 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -51,15 +51,6 @@ import javax.swing.DefaultListModel object FuzzierGrep : BackendStrategy { override val name = "fuzzier" private val fuzzierUtil = FuzzierUtil() - override fun supportsSecondaryField(): Boolean { - return true - } - - override fun buildCommand( - grepConfig: GrepConfig, - searchString: String, - secondarySearchString: String? - ): List = emptyList() override suspend fun handleSearch( grepConfig: GrepConfig, diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt index 9e8d55d4..9f72bf71 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt @@ -24,14 +24,19 @@ package com.mituuz.fuzzier.grep.backend +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.GrepConfig import com.mituuz.fuzzier.entities.RowContainer +import com.mituuz.fuzzier.runner.CommandRunner +import javax.swing.DefaultListModel object Ripgrep : BackendStrategy { override val name = "ripgrep" - override fun buildCommand( + private fun buildCommand( grepConfig: GrepConfig, searchString: String, secondarySearchString: String? @@ -64,12 +69,22 @@ object Ripgrep : BackendStrategy { return commands } - override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { + private fun parseOutputLine(line: String, projectBasePath: String): RowContainer? { val line = line.replace(projectBasePath, ".") return RowContainer.rgRowContainerFromString(line, projectBasePath) } - override fun supportsSecondaryField(): Boolean { - return true + override suspend fun handleSearch( + grepConfig: GrepConfig, + searchString: String, + secondarySearchString: String?, + commandRunner: CommandRunner, + listModel: DefaultListModel, + projectBasePath: String, + project: Project, + fileFilter: (VirtualFile) -> Boolean + ) { + val commands = buildCommand(grepConfig, searchString, secondarySearchString) + commandRunner.runCommandPopulateListModel(commands, listModel, projectBasePath, this::parseOutputLine) } } diff --git a/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt b/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt index 40523c14..9042f88c 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt @@ -25,7 +25,7 @@ package com.mituuz.fuzzier.runner import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.grep.backend.BackendStrategy +import com.mituuz.fuzzier.entities.RowContainer import javax.swing.DefaultListModel interface CommandRunner { @@ -38,6 +38,6 @@ interface CommandRunner { commands: List, listModel: DefaultListModel, projectBasePath: String, - backend: BackendStrategy + parseOutputLine: (String, String) -> RowContainer? ) } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt b/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt index 0e16c144..40a142c1 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt @@ -30,7 +30,7 @@ import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener import com.intellij.openapi.util.Key import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.grep.backend.BackendStrategy +import com.mituuz.fuzzier.entities.RowContainer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.swing.DefaultListModel @@ -83,7 +83,7 @@ class DefaultCommandRunner : CommandRunner { commands: List, listModel: DefaultListModel, projectBasePath: String, - backend: BackendStrategy + parseOutputLine: (String, String) -> RowContainer? ) { try { val commandLine = GeneralCommandLine(commands) @@ -100,7 +100,7 @@ class DefaultCommandRunner : CommandRunner { event.text.lines().forEach { line -> if (count >= MAX_NUMBER_OR_RESULTS) return@forEach if (line.isNotBlank()) { - val rowContainer = backend.parseOutputLine(line, projectBasePath) + val rowContainer = parseOutputLine(line, projectBasePath) if (rowContainer != null) { listModel.addElement(rowContainer) count++ From 5df4c232780430fed310f9091611bd87d74fcdb4 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 16:45:21 +0200 Subject: [PATCH 28/31] Remove unnecessary method --- src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt | 2 +- .../kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index 3b9a138d..d05b6e25 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -102,7 +102,7 @@ open class FuzzyGrep : FuzzyAction() { yield() defaultDoc = EditorFactory.getInstance().createDocument("") - val showSecondaryField = backend!!.supportsSecondaryField() && grepConfig.supportsSecondaryField + val showSecondaryField = grepConfig.supportsSecondaryField component = FuzzyFinderComponent( project = project, showSecondaryField = showSecondaryField ) diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt index a2237133..95b55d01 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt @@ -44,6 +44,4 @@ sealed interface BackendStrategy { project: Project, fileFilter: (VirtualFile) -> Boolean = { true } ) - - fun supportsSecondaryField(): Boolean = true } From 3c95b6fec01ca529f35b0421acf9021d89bec977 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 16:56:47 +0200 Subject: [PATCH 29/31] Move tests to a correct file --- .../mituuz/fuzzier/grep/backend/Ripgrep.kt | 2 +- .../fuzzier/grep/backend/RipgrepTest.kt | 140 +++++++++++++++++ .../fuzzier/search/BackendStrategyTest.kt | 146 ------------------ 3 files changed, 141 insertions(+), 147 deletions(-) create mode 100644 src/test/kotlin/com/mituuz/fuzzier/grep/backend/RipgrepTest.kt delete mode 100644 src/test/kotlin/com/mituuz/fuzzier/search/BackendStrategyTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt index 9f72bf71..7061937e 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/Ripgrep.kt @@ -36,7 +36,7 @@ import javax.swing.DefaultListModel object Ripgrep : BackendStrategy { override val name = "ripgrep" - private fun buildCommand( + internal fun buildCommand( grepConfig: GrepConfig, searchString: String, secondarySearchString: String? diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/backend/RipgrepTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/RipgrepTest.kt new file mode 100644 index 00000000..bcda71a2 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/RipgrepTest.kt @@ -0,0 +1,140 @@ +/* + * 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.backend + +import com.mituuz.fuzzier.entities.CaseMode +import com.mituuz.fuzzier.entities.GrepConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RipgrepTest { + @Test + fun `buildCommand should include basic ripgrep flags`() { + val config = GrepConfig( + targets = null, + caseMode = CaseMode.SENSITIVE, + ) + + val result = Ripgrep.buildCommand(config, "test", null) + + assertEquals( + listOf( + "rg", + "--no-heading", + "--color=never", + "-n", + "--with-filename", + "--column", + "test", + "." + ), + result + ) + } + + @Test + fun `buildCommand should include file extension glob flag without dot`() { + val config = GrepConfig( + targets = null, + caseMode = CaseMode.SENSITIVE, + ) + + val result = Ripgrep.buildCommand(config, "test", "java") + + assertEquals( + listOf( + "rg", + "--no-heading", + "--color=never", + "-n", + "--with-filename", + "--column", + "-g", + "*.java", + "test", + "." + ), + result + ) + } + + @Test + fun `buildCommand should include file extension glob flag with dot`() { + val config = GrepConfig( + targets = null, + caseMode = CaseMode.SENSITIVE, + ) + + val result = Ripgrep.buildCommand(config, "test", ".java") + + assertEquals( + listOf( + "rg", + "--no-heading", + "--color=never", + "-n", + "--with-filename", + "--column", + "-g", + "*.java", + "test", + "." + ), + result + ) + } + + @Test + fun `buildCommand should add smart-case and -F for insensitive mode`() { + val config = GrepConfig( + targets = null, + caseMode = CaseMode.INSENSITIVE, + ) + + val result = Ripgrep.buildCommand(config, "test", null) + + assertTrue(result.contains("--smart-case")) + assertTrue(result.contains("-F")) + } + + @Test + fun `buildCommand should not add smart-case for sensitive mode`() { + val config = GrepConfig( + targets = null, + caseMode = CaseMode.SENSITIVE, + ) + + val result = Ripgrep.buildCommand(config, "test", null) + + assertTrue(!result.contains("--smart-case")) + assertTrue(!result.contains("-F")) + } + + @Test + fun `name should return ripgrep`() { + assertEquals("ripgrep", Ripgrep.name) + } +} diff --git a/src/test/kotlin/com/mituuz/fuzzier/search/BackendStrategyTest.kt b/src/test/kotlin/com/mituuz/fuzzier/search/BackendStrategyTest.kt deleted file mode 100644 index e226e6d5..00000000 --- a/src/test/kotlin/com/mituuz/fuzzier/search/BackendStrategyTest.kt +++ /dev/null @@ -1,146 +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.search - -import com.mituuz.fuzzier.entities.CaseMode -import com.mituuz.fuzzier.entities.GrepConfig -import com.mituuz.fuzzier.grep.backend.BackendStrategy -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class BackendStrategyTest { - - @Nested - inner class RipgrepTest { - @Test - fun `buildCommand should include basic ripgrep flags`() { - val config = GrepConfig( - targets = null, - caseMode = CaseMode.SENSITIVE, - ) - - val result = com.mituuz.fuzzier.grep.backend.Ripgrep.buildCommand(config, "test", null) - - assertEquals( - listOf( - "rg", - "--no-heading", - "--color=never", - "-n", - "--with-filename", - "--column", - "test", - "." - ), - result - ) - } - - @Test - fun `buildCommand should include file extension glob flag without dot`() { - val config = GrepConfig( - targets = null, - caseMode = CaseMode.SENSITIVE, - ) - - val result = com.mituuz.fuzzier.grep.backend.Ripgrep.buildCommand(config, "test", "java") - - assertEquals( - listOf( - "rg", - "--no-heading", - "--color=never", - "-n", - "--with-filename", - "--column", - "-g", - "*.java", - "test", - "." - ), - result - ) - } - - @Test - fun `buildCommand should include file extension glob flag with dot`() { - val config = GrepConfig( - targets = null, - caseMode = CaseMode.SENSITIVE, - ) - - val result = com.mituuz.fuzzier.grep.backend.Ripgrep.buildCommand(config, "test", ".java") - - assertEquals( - listOf( - "rg", - "--no-heading", - "--color=never", - "-n", - "--with-filename", - "--column", - "-g", - "*.java", - "test", - "." - ), - result - ) - } - - @Test - fun `buildCommand should add smart-case and -F for insensitive mode`() { - val config = GrepConfig( - targets = null, - caseMode = CaseMode.INSENSITIVE, - ) - - val result = com.mituuz.fuzzier.grep.backend.Ripgrep.buildCommand(config, "test", null) - - assertTrue(result.contains("--smart-case")) - assertTrue(result.contains("-F")) - } - - @Test - fun `buildCommand should not add smart-case for sensitive mode`() { - val config = GrepConfig( - targets = null, - caseMode = CaseMode.SENSITIVE, - ) - - val result = com.mituuz.fuzzier.grep.backend.Ripgrep.buildCommand(config, "test", null) - - assertTrue(!result.contains("--smart-case")) - assertTrue(!result.contains("-F")) - } - - @Test - fun `name should return ripgrep`() { - assertEquals("ripgrep", com.mituuz.fuzzier.grep.backend.Ripgrep.name) - } - } -} \ No newline at end of file From 18e2b068b106a8957cacd3f910fee13d885e8958 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 17:07:01 +0200 Subject: [PATCH 30/31] Collapse redudant interface to a concrete class --- .../com/mituuz/fuzzier/grep/FuzzyGrep.kt | 4 +- .../mituuz/fuzzier/runner/CommandRunner.kt | 90 ++++++++++++- .../fuzzier/runner/DefaultCommandRunner.kt | 127 ------------------ .../fuzzier/search/BackendResolverTest.kt | 5 +- 4 files changed, 93 insertions(+), 133 deletions(-) delete mode 100644 src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt index d05b6e25..8159b247 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt @@ -48,7 +48,7 @@ import com.mituuz.fuzzier.entities.RowContainer import com.mituuz.fuzzier.grep.backend.BackendResolver import com.mituuz.fuzzier.grep.backend.BackendStrategy import com.mituuz.fuzzier.intellij.files.FileOpeningUtil -import com.mituuz.fuzzier.runner.DefaultCommandRunner +import com.mituuz.fuzzier.runner.CommandRunner import com.mituuz.fuzzier.ui.bindings.ActivationBindings import com.mituuz.fuzzier.ui.popup.PopupConfig import com.mituuz.fuzzier.ui.preview.CoroutinePreviewAlarmProvider @@ -65,7 +65,7 @@ open class FuzzyGrep : FuzzyAction() { val isWindows = System.getProperty("os.name").lowercase().contains("win") private val backendResolver = BackendResolver(isWindows) - private val commandRunner = DefaultCommandRunner() + private val commandRunner = CommandRunner() private var currentLaunchJob: Job? = null private var backend: BackendStrategy? = null private var previewAlarm: SingleAlarm? = null diff --git a/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt b/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt index 9042f88c..1f05e5b8 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/runner/CommandRunner.kt @@ -24,20 +24,104 @@ package com.mituuz.fuzzier.runner +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessListener +import com.intellij.openapi.util.Key import com.mituuz.fuzzier.entities.FuzzyContainer import com.mituuz.fuzzier.entities.RowContainer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.swing.DefaultListModel -interface CommandRunner { +class CommandRunner { + companion object { + const val MAX_OUTPUT_SIZE = 10000 + const val MAX_NUMBER_OR_RESULTS = 1000 + } + suspend fun runCommandForOutput( commands: List, projectBasePath: String - ): String? + ): String? { + return try { + val commandLine = GeneralCommandLine(commands) + .withWorkDirectory(projectBasePath) + .withRedirectErrorStream(true) + val output = StringBuilder() + val processHandler = OSProcessHandler(commandLine) + + processHandler.addProcessListener(object : ProcessListener { + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + if (output.length < MAX_OUTPUT_SIZE) { + output.appendLine(event.text.replace("\n", "")) + } + } + }) + try { + withContext(Dispatchers.IO) { + processHandler.startNotify() + processHandler.waitFor(2000) + } + } finally { + if (!processHandler.isProcessTerminated) { + processHandler.destroyProcess() + } + } + output.toString() + } catch (_: InterruptedException) { + throw InterruptedException() + } + } + + /** + * Run the command and stream a limited number of results to the list model + */ suspend fun runCommandPopulateListModel( commands: List, listModel: DefaultListModel, projectBasePath: String, parseOutputLine: (String, String) -> RowContainer? - ) + ) { + try { + val commandLine = GeneralCommandLine(commands) + .withWorkDirectory(projectBasePath) + .withRedirectErrorStream(true) + + val processHandler = OSProcessHandler(commandLine) + var count = 0 + + processHandler.addProcessListener(object : ProcessListener { + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + if (count >= MAX_NUMBER_OR_RESULTS) return + + event.text.lines().forEach { line -> + if (count >= MAX_NUMBER_OR_RESULTS) return@forEach + if (line.isNotBlank()) { + val rowContainer = parseOutputLine(line, projectBasePath) + if (rowContainer != null) { + listModel.addElement(rowContainer) + count++ + } + } + } + } + }) + + try { + withContext(Dispatchers.IO) { + processHandler.startNotify() + processHandler.waitFor(2000) + } + } finally { + if (!processHandler.isProcessTerminated) { + processHandler.destroyProcess() + } + } + } catch (_: InterruptedException) { + throw InterruptedException() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt b/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt deleted file mode 100644 index 40a142c1..00000000 --- a/src/main/kotlin/com/mituuz/fuzzier/runner/DefaultCommandRunner.kt +++ /dev/null @@ -1,127 +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.runner - -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.OSProcessHandler -import com.intellij.execution.process.ProcessEvent -import com.intellij.execution.process.ProcessListener -import com.intellij.openapi.util.Key -import com.mituuz.fuzzier.entities.FuzzyContainer -import com.mituuz.fuzzier.entities.RowContainer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import javax.swing.DefaultListModel - -class DefaultCommandRunner : CommandRunner { - companion object { - const val MAX_OUTPUT_SIZE = 10000 - const val MAX_NUMBER_OR_RESULTS = 1000 - } - - override suspend fun runCommandForOutput( - commands: List, - projectBasePath: String - ): String? { - return try { - val commandLine = GeneralCommandLine(commands) - .withWorkDirectory(projectBasePath) - .withRedirectErrorStream(true) - val output = StringBuilder() - val processHandler = OSProcessHandler(commandLine) - - processHandler.addProcessListener(object : ProcessListener { - override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { - if (output.length < MAX_OUTPUT_SIZE) { - output.appendLine(event.text.replace("\n", "")) - } - } - }) - - try { - withContext(Dispatchers.IO) { - processHandler.startNotify() - processHandler.waitFor(2000) - } - } finally { - if (!processHandler.isProcessTerminated) { - processHandler.destroyProcess() - } - } - output.toString() - } catch (_: InterruptedException) { - throw InterruptedException() - } - } - - /** - * Run the command and stream a limited number of results to the list model - */ - override suspend fun runCommandPopulateListModel( - commands: List, - listModel: DefaultListModel, - projectBasePath: String, - parseOutputLine: (String, String) -> RowContainer? - ) { - try { - val commandLine = GeneralCommandLine(commands) - .withWorkDirectory(projectBasePath) - .withRedirectErrorStream(true) - - val processHandler = OSProcessHandler(commandLine) - var count = 0 - - processHandler.addProcessListener(object : ProcessListener { - override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { - if (count >= MAX_NUMBER_OR_RESULTS) return - - event.text.lines().forEach { line -> - if (count >= MAX_NUMBER_OR_RESULTS) return@forEach - if (line.isNotBlank()) { - val rowContainer = parseOutputLine(line, projectBasePath) - if (rowContainer != null) { - listModel.addElement(rowContainer) - count++ - } - } - } - } - }) - - try { - withContext(Dispatchers.IO) { - processHandler.startNotify() - processHandler.waitFor(2000) - } - } finally { - if (!processHandler.isProcessTerminated) { - processHandler.destroyProcess() - } - } - } catch (_: InterruptedException) { - throw InterruptedException() - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/mituuz/fuzzier/search/BackendResolverTest.kt b/src/test/kotlin/com/mituuz/fuzzier/search/BackendResolverTest.kt index 05675268..818b32d6 100644 --- a/src/test/kotlin/com/mituuz/fuzzier/search/BackendResolverTest.kt +++ b/src/test/kotlin/com/mituuz/fuzzier/search/BackendResolverTest.kt @@ -25,6 +25,7 @@ package com.mituuz.fuzzier.search import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.TestApplicationManager import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.mituuz.fuzzier.grep.backend.BackendResolver import com.mituuz.fuzzier.grep.backend.FuzzierGrep @@ -41,12 +42,14 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class BackendResolverTest { + @Suppress("unused") + private val testApplicationManager: TestApplicationManager = TestApplicationManager.getInstance() private lateinit var commandRunner: CommandRunner private val projectBasePath = "/test/project" @BeforeEach fun setUp() { - commandRunner = mockk() + commandRunner = mockk(relaxed = true) } @Test From fb26702614536443cd2bbe07e925c057e6d2b7b1 Mon Sep 17 00:00:00 2001 From: Mitja Date: Sun, 18 Jan 2026 17:20:32 +0200 Subject: [PATCH 31/31] Separate utils and create tests --- .../fuzzier/grep/backend/FuzzierGrep.kt | 37 ++--- .../fuzzier/grep/backend/ResultBatcher.kt | 55 ++++++ .../fuzzier/grep/backend/SearchMatcher.kt | 52 ++++++ .../fuzzier/grep/backend/ResultBatcherTest.kt | 157 ++++++++++++++++++ .../fuzzier/grep/backend/SearchMatcherTest.kt | 123 ++++++++++++++ 5 files changed, 399 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcher.kt create mode 100644 src/main/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcher.kt create mode 100644 src/test/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcherTest.kt create mode 100644 src/test/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcherTest.kt diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt index 0f7c8568..332d3fe6 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt @@ -51,6 +51,7 @@ import javax.swing.DefaultListModel object FuzzierGrep : BackendStrategy { override val name = "fuzzier" private val fuzzierUtil = FuzzierUtil() + private val searchMatcher = SearchMatcher() override suspend fun handleSearch( grepConfig: GrepConfig, @@ -64,10 +65,8 @@ object FuzzierGrep : BackendStrategy { ) { val files = grepConfig.targets ?: collectFiles(searchString, fileFilter, project, grepConfig) - var count = 0 - val batchSize = 20 - val currentBatch = mutableListOf() val maxResults = service().state.fileListLimit + val batcher = ResultBatcher() for (file in files) { currentCoroutineContext().ensureActive() @@ -81,11 +80,7 @@ object FuzzierGrep : BackendStrategy { for ((index, line) in lines.withIndex()) { currentCoroutineContext().ensureActive() - val found = if (grepConfig.caseMode == CaseMode.INSENSITIVE) { - line.contains(searchString, ignoreCase = true) - } else { - line.contains(searchString) - } + val found = searchMatcher.matchesLine(line, searchString, grepConfig.caseMode) if (found) { val (filePath, basePath) = fuzzierUtil.extractModulePath(file.path, project) @@ -111,26 +106,22 @@ object FuzzierGrep : BackendStrategy { for (match in matches) { currentCoroutineContext().ensureActive() - currentBatch.add(match) - count++ - - if (currentBatch.size >= batchSize) { - val toAdd = currentBatch.toList() - currentBatch.clear() + val batch = batcher.add(match) + if (batch != null) { withContext(Dispatchers.Main) { - listModel.addAll(toAdd) + listModel.addAll(batch) } } - if (count >= maxResults) break + if (batcher.getCount() >= maxResults) break } - if (count >= maxResults) break + if (batcher.getCount() >= maxResults) break } - if (currentBatch.isNotEmpty()) { + if (batcher.hasRemaining()) { withContext(Dispatchers.Main) { - listModel.addAll(currentBatch) + listModel.addAll(batcher.getRemaining()) } } } @@ -142,13 +133,9 @@ object FuzzierGrep : BackendStrategy { grepConfig: GrepConfig ): List { val files = mutableListOf() - val trimmedSearch = searchString.trim() - val words = trimmedSearch.split(" ") - - if (words.size > 1 && words[1].isNotEmpty() && trimmedSearch.count { it == ' ' } >= 2) { - // Extract the first complete word - the only one we can be sure is a full word - val firstCompleteWord = words[1] + val firstCompleteWord = searchMatcher.extractFirstCompleteWord(searchString) + if (firstCompleteWord != null) { ReadAction.run { val helper = PsiSearchHelper.getInstance(project) helper.processAllFilesWithWord( diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcher.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcher.kt new file mode 100644 index 00000000..7f749edc --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcher.kt @@ -0,0 +1,55 @@ +/* + * 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.backend + +class ResultBatcher( + private val batchSize: Int = 20 +) { + private val currentBatch = mutableListOf() + private var count = 0 + + fun add(item: T): List? { + currentBatch.add(item) + count++ + + if (currentBatch.size >= batchSize) { + val batch = currentBatch.toList() + currentBatch.clear() + return batch + } + + return null + } + + fun getRemaining(): List { + val batch = currentBatch.toList() + currentBatch.clear() + return batch + } + + fun getCount(): Int = count + + fun hasRemaining(): Boolean = currentBatch.isNotEmpty() +} diff --git a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcher.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcher.kt new file mode 100644 index 00000000..62df0b93 --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcher.kt @@ -0,0 +1,52 @@ +/* + * 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.backend + +import com.mituuz.fuzzier.entities.CaseMode + +class SearchMatcher { + fun matchesLine(line: String, searchString: String, caseMode: CaseMode): Boolean { + return if (caseMode == CaseMode.INSENSITIVE) { + line.contains(searchString, ignoreCase = true) + } else { + line.contains(searchString) + } + } + + fun parseSearchWords(searchString: String): List { + return searchString.trim().split(" ") + } + + fun extractFirstCompleteWord(searchString: String): String? { + val trimmedSearch = searchString.trim() + val words = parseSearchWords(trimmedSearch) + + if (words.size > 1 && words[1].isNotEmpty() && trimmedSearch.count { it == ' ' } >= 2) { + return words[1] + } + + return null + } +} diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcherTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcherTest.kt new file mode 100644 index 00000000..f3d95968 --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/ResultBatcherTest.kt @@ -0,0 +1,157 @@ +/* + * 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.backend + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class ResultBatcherTest { + @Test + fun `add should return null when batch size not reached`() { + val batcher = ResultBatcher(batchSize = 3) + assertNull(batcher.add("item1")) + assertNull(batcher.add("item2")) + } + + @Test + fun `add should return batch when batch size reached`() { + val batcher = ResultBatcher(batchSize = 3) + batcher.add("item1") + batcher.add("item2") + val result = batcher.add("item3") + + assertEquals(listOf("item1", "item2", "item3"), result) + } + + @Test + fun `add should clear batch after returning`() { + val batcher = ResultBatcher(batchSize = 2) + batcher.add("item1") + val result = batcher.add("item2") + + assertNotNull(result) + assertFalse(batcher.hasRemaining()) + } + + @Test + fun `getRemaining should return items that have not been batched`() { + val batcher = ResultBatcher(batchSize = 3) + batcher.add("item1") + batcher.add("item2") + + val remaining = batcher.getRemaining() + assertEquals(listOf("item1", "item2"), remaining) + } + + @Test + fun `getRemaining should clear internal batch`() { + val batcher = ResultBatcher(batchSize = 3) + batcher.add("item1") + batcher.getRemaining() + + assertFalse(batcher.hasRemaining()) + } + + @Test + fun `getRemaining should return empty list when no items`() { + val batcher = ResultBatcher(batchSize = 3) + assertTrue(batcher.getRemaining().isEmpty()) + } + + @Test + fun `getCount should track total items added`() { + val batcher = ResultBatcher(batchSize = 2) + batcher.add("item1") + assertEquals(1, batcher.getCount()) + + batcher.add("item2") + assertEquals(2, batcher.getCount()) + + batcher.add("item3") + assertEquals(3, batcher.getCount()) + } + + @Test + fun `hasRemaining should return true when items in batch`() { + val batcher = ResultBatcher(batchSize = 3) + batcher.add("item1") + assertTrue(batcher.hasRemaining()) + } + + @Test + fun `hasRemaining should return false when batch is empty`() { + val batcher = ResultBatcher(batchSize = 3) + assertFalse(batcher.hasRemaining()) + } + + @Test + fun `hasRemaining should return false after batch is returned`() { + val batcher = ResultBatcher(batchSize = 2) + batcher.add("item1") + batcher.add("item2") + assertFalse(batcher.hasRemaining()) + } + + @Test + fun `should handle default batch size of 20`() { + val batcher = ResultBatcher() + + for (i in 1..19) { + assertNull(batcher.add("item$i")) + } + + val result = batcher.add("item20") + assertNotNull(result) + assertEquals(20, result?.size) + } + + @Test + fun `should work with different types`() { + val batcher = ResultBatcher(batchSize = 2) + batcher.add(1) + val result = batcher.add(2) + + assertEquals(listOf(1, 2), result) + } + + @Test + fun `multiple batches should work correctly`() { + val batcher = ResultBatcher(batchSize = 2) + + val batch1 = batcher.add("item1") + assertNull(batch1) + + val batch2 = batcher.add("item2") + assertEquals(listOf("item1", "item2"), batch2) + + val batch3 = batcher.add("item3") + assertNull(batch3) + + val batch4 = batcher.add("item4") + assertEquals(listOf("item3", "item4"), batch4) + + assertEquals(4, batcher.getCount()) + } +} diff --git a/src/test/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcherTest.kt b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcherTest.kt new file mode 100644 index 00000000..5d3132bb --- /dev/null +++ b/src/test/kotlin/com/mituuz/fuzzier/grep/backend/SearchMatcherTest.kt @@ -0,0 +1,123 @@ +/* + * 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.backend + +import com.mituuz.fuzzier.entities.CaseMode +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class SearchMatcherTest { + private val matcher = SearchMatcher() + + @Test + fun `matchesLine should return true for exact match with case sensitive`() { + assertTrue(matcher.matchesLine("hello world", "hello", CaseMode.SENSITIVE)) + } + + @Test + fun `matchesLine should return false for case mismatch with case sensitive`() { + assertFalse(matcher.matchesLine("hello world", "HELLO", CaseMode.SENSITIVE)) + } + + @Test + fun `matchesLine should return true for case mismatch with case insensitive`() { + assertTrue(matcher.matchesLine("hello world", "HELLO", CaseMode.INSENSITIVE)) + } + + @Test + fun `matchesLine should return true for lowercase match with case insensitive`() { + assertTrue(matcher.matchesLine("HELLO WORLD", "hello", CaseMode.INSENSITIVE)) + } + + @Test + fun `matchesLine should return false when substring not found`() { + assertFalse(matcher.matchesLine("hello world", "xyz", CaseMode.SENSITIVE)) + } + + @Test + fun `matchesLine should handle empty string`() { + assertTrue(matcher.matchesLine("hello world", "", CaseMode.SENSITIVE)) + } + + @Test + fun `parseSearchWords should split by spaces`() { + val result = matcher.parseSearchWords("hello world test") + assertEquals(listOf("hello", "world", "test"), result) + } + + @Test + fun `parseSearchWords should trim input`() { + val result = matcher.parseSearchWords(" hello world ") + assertEquals(listOf("hello", "world"), result) + } + + @Test + fun `parseSearchWords should handle single word`() { + val result = matcher.parseSearchWords("hello") + assertEquals(listOf("hello"), result) + } + + @Test + fun `parseSearchWords should handle empty string`() { + val result = matcher.parseSearchWords("") + assertEquals(listOf(""), result) + } + + @Test + fun `extractFirstCompleteWord should return second word when enough spaces`() { + val result = matcher.extractFirstCompleteWord("hello world test") + assertEquals("world", result) + } + + @Test + fun `extractFirstCompleteWord should return null when less than 2 spaces`() { + val result = matcher.extractFirstCompleteWord("hello world") + assertNull(result) + } + + @Test + fun `extractFirstCompleteWord should return null when only one word`() { + val result = matcher.extractFirstCompleteWord("hello") + assertNull(result) + } + + @Test + fun `extractFirstCompleteWord should return null when second word is empty`() { + val result = matcher.extractFirstCompleteWord("hello test") + assertNull(result) + } + + @Test + fun `extractFirstCompleteWord should handle multiple spaces`() { + val result = matcher.extractFirstCompleteWord("hello world test foo") + assertEquals("world", result) + } + + @Test + fun `extractFirstCompleteWord should trim input`() { + val result = matcher.extractFirstCompleteWord(" hello world test ") + assertEquals("world", result) + } +}