diff --git a/.gitignore b/.gitignore index 2fa7b6b4..780439f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .kotlin build/ +.kotlin/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/build.gradle.kts b/build.gradle.kts index 48ecd49a..5d5109da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { zipSigner() testFramework(TestFrameworkType.Platform) + + bundledPlugin("com.intellij.java") } testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.4") diff --git a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt index 3295e853..85ce9678 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/Fuzzier.kt @@ -136,7 +136,7 @@ open class Fuzzier : FuzzyAction() { /** * Populates the file list with recently opened files */ - private fun createInitialView(project: Project) { + open fun createInitialView(project: Project) { ApplicationManager.getApplication().executeOnPooledThread { val editorHistoryManager = EditorHistoryManager.getInstance(project) @@ -301,7 +301,7 @@ open class Fuzzier : FuzzyAction() { popup?.cancel() } - private fun createListeners(project: Project) { + open fun createListeners(project: Project) { // Add a listener that updates the contents of the preview pane component.fileList.addListSelectionListener { event -> if (!event.valueIsAdjusting) { diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzierFS.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzierFS.kt new file mode 100644 index 00000000..e563154b --- /dev/null +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzierFS.kt @@ -0,0 +1,243 @@ +/* +MIT License + +Copyright (c) 2024 Mitja Leino + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package com.mituuz.fuzzier + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ScrollType +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.findPsiFile +import com.intellij.psi.* +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.mituuz.fuzzier.components.FuzzyFinderComponent +import com.mituuz.fuzzier.entities.FuzzyMatchContainer +import com.mituuz.fuzzier.entities.FuzzyMatchContainer.FuzzyScore +import com.mituuz.fuzzier.entities.ScoreCalculator +import org.jetbrains.uast.* +import org.jetbrains.uast.visitor.AbstractUastVisitor +import javax.swing.DefaultListModel + +class FuzzierFS : Fuzzier() { + override var title: String = "Fuzzy Search (File Structure)" + override fun updateListContents(project: Project, searchString: String) { + component.isFs = true + val fileEditorManager = FileEditorManager.getInstance(project) + val currentEditor = fileEditorManager.selectedEditor + + val listModel = DefaultListModel() + + if (currentEditor != null) { + ApplicationManager.getApplication().runReadAction() { + val psiFile: PsiFile? = currentEditor.file.findPsiFile(project) + + if (psiFile != null) { + val uFile = UastFacade.convertElementWithParent(psiFile, UFile::class.java) + uFile?.accept(getVisitor(listModel, searchString)) + + ProgressManager.getInstance().run { + component.fileList.model = listModel + component.fileList.cellRenderer = getCellRenderer() + component.fileList.setPaintBusy(false) + if (!component.fileList.isEmpty) { + component.fileList.setSelectedValue(listModel[0], true) + } + } + } + } + } + } + + override fun createListeners(project: Project) { + // Add a listener that updates the contents of the preview pane + component.fileList.addListSelectionListener { event -> + if (!event.valueIsAdjusting) { + if (component.fileList.isEmpty) { + ApplicationManager.getApplication().invokeLater { + val previewPane = (component as FuzzyFinderComponent).previewPane + previewPane.updateFile(EditorFactory.getInstance().createDocument("")) + } + return@addListSelectionListener + } + val selectedValue = component.fileList.selectedValue + val currentFile = FileEditorManager.getInstance(project).selectedEditor?.file + + ProgressManager.getInstance().run(object : Task.Backgroundable(null, "Loading file", false) { + override fun run(indicator: ProgressIndicator) { + ApplicationManager.getApplication().invokeLater { + val previewPane = (component as FuzzyFinderComponent).previewPane + previewPane.updateFile(currentFile, selectedValue?.fileOffset) + } + } + }) + } + } + } + + override fun createInitialView(project: Project) { + ApplicationManager.getApplication().executeOnPooledThread { + component.isFs = true + val fileEditorManager = FileEditorManager.getInstance(project) + val currentEditor = fileEditorManager.selectedEditor + + val listModel = DefaultListModel() + + if (currentEditor != null) { + ApplicationManager.getApplication().runReadAction() { + val psiFile: PsiFile? = currentEditor.file.findPsiFile(project) + + if (psiFile != null) { + val uFile = UastFacade.convertElementWithParent(psiFile, UFile::class.java) + uFile?.accept(getOpenVisitor(listModel)) + + ProgressManager.getInstance().run { + component.fileList.model = listModel + component.fileList.cellRenderer = getCellRenderer() + component.fileList.setPaintBusy(false) + if (!component.fileList.isEmpty) { + component.fileList.setSelectedValue(listModel[0], true) + } + } + } + } + } + } + } + + private fun getOpenVisitor(listModel: DefaultListModel): AbstractUastVisitor { + return object : AbstractUastVisitor() { + override fun visitClass(node: UClass): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createStaticContainer(listModel, name, displayString, node.textRange) + return super.visitClass(node) + } + + override fun visitMethod(node: UMethod): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createStaticContainer(listModel, name, displayString, node.textRange) + return super.visitMethod(node) + } + + override fun visitVariable(node: UVariable): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createStaticContainer(listModel, name, displayString, node.textRange) + return super.visitVariable(node) + } + } + } + + private fun createStaticContainer(listModel: DefaultListModel, name: String?, + displayString: String?, textRange: com.intellij.openapi.util.TextRange?) { + if (name.isNullOrBlank() || displayString == null) { + return + } + val container = FuzzyMatchContainer(FuzzyScore(), displayString, name) + if (textRange != null) { + container.fileOffset = textRange.startOffset + } + listModel.addElement(container) + } + + private fun getVisitor(listModel: DefaultListModel, + searchString: String = ""): AbstractUastVisitor { + return object : AbstractUastVisitor() { + override fun visitClass(node: UClass): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createContainer(listModel, searchString, displayString, name, node.textRange) + return super.visitClass(node) + } + + override fun visitMethod(node: UMethod): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createContainer(listModel, searchString, displayString, name, node.textRange) + return super.visitMethod(node) + } + + override fun visitVariable(node: UVariable): Boolean { + val name = node.name + val displayString = getTextRepresentation(node, name) + createContainer(listModel, searchString, displayString, name, node.textRange) + return super.visitVariable(node) + } + } + } + + private fun getTextRepresentation(uElement: UElement, name: String?): String? { + if (name.isNullOrBlank()) { + return null; + } + when (uElement) { + is UVariable -> { + val type = uElement.type.presentableText + return "Variable: $name: $type" + } + is UMethod -> { + val params: List = uElement.uastParameters + var paramString = "" + var returnString = "" + if (params.isNotEmpty()) { + paramString = "(" + for (param: UParameter in params) { + paramString = "$paramString${param.name}: ${(param.type as PsiClassReferenceType).name}, " + } + paramString = paramString.removeSuffix(", ") + paramString = "$paramString)" + } else { + paramString = "()" + } + val returnType = uElement.returnType + if (returnType != null && returnType.presentableText != "void") { + returnString = ": ${returnType.presentableText}" + } + return "Method: $name$paramString$returnString" + } + is UClass -> return "Class: $name" + } + return null + } + + private fun createContainer(listModel: DefaultListModel, searchString: String, + displayString: String?, name: String?, textRange: com.intellij.openapi.util.TextRange?) { + if (name.isNullOrBlank() || displayString == null) { + return; + } + val scoreCalculator = ScoreCalculator(searchString) + val fs = scoreCalculator.calculateScore(name) + if (fs != null) { + val container = FuzzyMatchContainer(fs, displayString, name) + if (textRange != null) { + container.fileOffset = textRange.startOffset + } + listModel.addElement(container) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt b/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt index c40247db..b04e65b9 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/FuzzyAction.kt @@ -178,8 +178,9 @@ abstract class FuzzyAction : AnAction() { val renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) as JLabel val container = value as FuzzyMatchContainer - val filenameType: FilenameType = if (component.isDirSelector) { - FILE_PATH_ONLY // Directories are always shown as full paths + val filenameType: FilenameType = if (component.isDirSelector || component.isFs) { + // Directories and file structures are always shown as full paths + FILE_PATH_ONLY } else { fuzzierSettingsService.state.filenameType } diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt index d909192e..d99f6cd3 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/FuzzyComponent.kt @@ -32,4 +32,5 @@ open class FuzzyComponent : JPanel() { var fileList = JBList() var searchField = EditorTextField() var isDirSelector = false + var isFs = false } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/components/PreviewEditor.kt b/src/main/kotlin/com/mituuz/fuzzier/components/PreviewEditor.kt index 85a496a4..27791eea 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/components/PreviewEditor.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/components/PreviewEditor.kt @@ -28,6 +28,7 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.FileDocumentManager @@ -56,7 +57,12 @@ class PreviewEditor(project: Project?) : EditorTextField( } override fun createEditor(): EditorEx { - val editor = super.createEditor() + val editor = super.createEditor() // TODO: Exception in thread com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments: Access is allowed from Event Dispatch Thread (EDT) only; see https://jb.gg/ij-platform-threading for details editor.setVerticalScrollbarVisible(true) + /* + at com.mituuz.fuzzier.components.PreviewEditor.updateFile(PreviewEditor.kt:78) + at com.mituuz.fuzzier.Fuzzier.createListeners$lambda$12(Fuzzier.kt:234) + at com.mituuz.fuzzier.FuzzierFS.updateListContents$lambda$1(FuzzierFS.kt:39) + */ editor.setVerticalScrollbarVisible(true) editor.setHorizontalScrollbarVisible(true) editor.isOneLineMode = false @@ -79,7 +85,7 @@ class PreviewEditor(project: Project?) : EditorTextField( this.fileType = PlainTextFileType.INSTANCE } - fun updateFile(virtualFile: VirtualFile?) { + fun updateFile(virtualFile: VirtualFile?, offset: Int? = null) { ApplicationManager.getApplication().executeOnPooledThread { val sourceDocument = ApplicationManager.getApplication().runReadAction { virtualFile?.let { FileDocumentManager.getInstance().getDocument(virtualFile) } @@ -101,9 +107,9 @@ class PreviewEditor(project: Project?) : EditorTextField( this.document = sourceDocument } ApplicationManager.getApplication().invokeLater { - editor?.scrollingModel?.run { - scrollHorizontally(0) - scrollVertically(0) + scrollTo0() + if (offset != null) { + moveCaretToOffset(offset) } } } @@ -114,4 +120,20 @@ class PreviewEditor(project: Project?) : EditorTextField( } } } + + fun scrollTo0() { + editor?.scrollingModel?.run { + scrollHorizontally(0) + scrollVertically(0) + } + } + + fun moveCaretToOffset(offset: Int) { + val caret = editor?.caretModel?.primaryCaret + if (caret != null) { + caret.moveToOffset(offset) + val logicalPosition = caret.logicalPosition + editor?.scrollingModel?.scrollTo(logicalPosition, ScrollType.CENTER) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt index 5c65887e..e40b18d4 100644 --- a/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt +++ b/src/main/kotlin/com/mituuz/fuzzier/entities/FuzzyMatchContainer.kt @@ -45,8 +45,14 @@ class FuzzyMatchContainer( ) : Serializable { @Transient private var initialPath: String? = null + var fileOffset: Int? = null companion object { + /** + * Used for showing recent files + * + * Creates a fuzzy match container with explicitly specified score. + */ fun createOrderedContainer( order: Int, filePath: String, diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index baf0955c..10a78045 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -11,7 +11,12 @@ - com.intellij.modules.platform + + com.intellij.modules.platform + + + com.intellij.modules.java + @@ -44,6 +49,11 @@ + +