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
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/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/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/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/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/entities/RowContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/RowContainer.kt
index c3f7cd7e..df7d67e8 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/FuzzyGrep.kt b/src/main/kotlin/com/mituuz/fuzzier/grep/FuzzyGrep.kt
index 88a7ea37..ecd0c566 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
@@ -72,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",
)
@@ -102,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)
@@ -155,8 +156,7 @@ open class FuzzyGrep : FuzzyAction() {
try {
val results = withContext(Dispatchers.IO) {
findInFiles(
- searchString,
- project
+ searchString, project
)
}
coroutineContext.ensureActive()
@@ -172,17 +172,35 @@ 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!!)
+ backend!!.handleSearch(
+ grepConfig, searchString, secondaryFieldText, commandRunner, listModel, projectBasePath, project
+ ) { vf -> validVf(vf, secondaryFieldText, ChangeListManager.getInstance(project)) }
}
return listModel
}
+ private fun validVf(
+ virtualFile: VirtualFile, secondaryFieldText: String? = null, clm: ChangeListManager
+ ): Boolean {
+ if (virtualFile.isDirectory) return false
+ if (virtualFile.fileType.isBinary) return false
+
+ 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) {
// Add a listener that updates the contents of the preview pane
component.fileList.addListSelectionListener { event ->
@@ -199,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 {
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 f8f01570..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,23 +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 {
- 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"))
+ val grepBackendSetting = service().state.grepBackend
+
+ 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 3cf53d48..9911cb2d 100644
--- a/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt
+++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/BackendStrategy.kt
@@ -24,9 +24,14 @@
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
sealed interface BackendStrategy {
val name: String
@@ -36,104 +41,65 @@ sealed interface BackendStrategy {
return RowContainer.rowContainerFromString(line, projectBasePath)
}
- fun supportsSecondaryField(): Boolean = false
+ suspend fun handleSearch(
+ grepConfig: GrepConfig,
+ searchString: String,
+ secondarySearchString: String?,
+ 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)
+ }
- object Ripgrep : BackendStrategy {
- override val name = "ripgrep"
+ fun supportsSecondaryField(): Boolean = false
+}
- override fun buildCommand(
- grepConfig: GrepConfig,
- searchString: String,
- secondarySearchString: String?
- ): List {
- val commands = mutableListOf("rg")
+object Ripgrep : BackendStrategy {
+ override val name = "ripgrep"
- if (grepConfig.caseMode == CaseMode.INSENSITIVE) {
- commands.add("--smart-case")
- commands.add("-F")
- }
+ override fun buildCommand(
+ grepConfig: GrepConfig,
+ searchString: String,
+ secondarySearchString: String?
+ ): List {
+ val commands = mutableListOf("rg")
- 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
+ if (grepConfig.caseMode == CaseMode.INSENSITIVE) {
+ commands.add("--smart-case")
+ commands.add("-F")
}
- override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? {
- val line = line.replace(projectBasePath, ".")
- return RowContainer.rgRowContainerFromString(line, projectBasePath)
+ 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)
- override fun supportsSecondaryField(): Boolean {
- return true
- }
+ // Convert VirtualFiles to paths, or use "." if targets is null
+ val targetPaths = grepConfig.targets?.map { it.path } ?: listOf(".")
+ commands.addAll(targetPaths)
+ return commands
}
- 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
- }
+ override fun parseOutputLine(line: String, projectBasePath: String): RowContainer? {
+ val line = line.replace(projectBasePath, ".")
+ return RowContainer.rgRowContainerFromString(line, projectBasePath)
}
- 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
- }
+ override fun supportsSecondaryField(): Boolean {
+ return 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
new file mode 100644
index 00000000..6dec316c
--- /dev/null
+++ b/src/main/kotlin/com/mituuz/fuzzier/grep/backend/FuzzierGrep.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.components.service
+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.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 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 supportsSecondaryField(): Boolean {
+ return true
+ }
+
+ 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 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()
+
+ 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) {
+ val (filePath, basePath) = fuzzierUtil.extractModulePath(file.path, project)
+ fileMatches.add(
+ RowContainer(
+ filePath,
+ basePath,
+ file.name,
+ index,
+ line.trim(),
+ virtualFile = file
+ )
+ )
+ }
+ }
+ 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
+ }
+
+ 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)
+ }
+ }
+ }
+
+ private suspend fun collectFiles(
+ searchString: String,
+ fileFilter: (VirtualFile) -> Boolean,
+ project: Project,
+ grepConfig: GrepConfig
+ ): List {
+ val files = mutableListOf()
+ val trimmedSearch = searchString.trim()
+
+ 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 (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)
+ }
+ }
+ true
+ },
+ grepConfig.caseMode == CaseMode.SENSITIVE
+ )
+ }
+ }
+ } else {
+ 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 }
+ }
+
+ return collectFiles(
+ targets = indexTargets,
+ shouldContinue = { job.isActive },
+ fileFilter = fileFilter
+ )
+ }
+
+ return files
+ }
+
+ 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
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()
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