Skip to content

Commit

Permalink
feat(icon-associations): add file-based indexing for efficient icon a…
Browse files Browse the repository at this point in the history
…ssociation retrieval
  • Loading branch information
mallowigi committed Dec 21, 2024
1 parent 837a98f commit c1e50a8
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2023 Elior "Mallowigi" Boukhobza
* Copyright (c) 2015-2024 Elior "Mallowigi" Boukhobza
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -20,7 +20,6 @@
* 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.mallowigi.icons.associations

Expand All @@ -39,10 +38,8 @@ import java.io.Serializable
* @property iconType the [IconType] of icon (file/folder/psi)
* @property name the name of the association
* @property icon the icon path
* @property priority association priority. Lowest priorities are used
* last.
* @property matcher How the association will be matched against (regex,
* type)
* @property priority association priority. Lowest priorities are used last.
* @property matcher How the association will be matched against (regex, type)
* @property isEmpty whether the association has empty fields
* @property iconColor the color of the icon
* @property folderIconColor the color of the folder icon
Expand Down Expand Up @@ -129,6 +126,34 @@ abstract class Association @PropertyMapping() internal constructor() : Serializa
/** Check if matches icon name. */
fun matchesName(assocName: String): Boolean = name == assocName

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Association

if (enabled != other.enabled) return false
if (touched != other.touched) return false
if (priority != other.priority) return false
if (iconType != other.iconType) return false
if (name != other.name) return false
if (icon != other.icon) return false
if (matcher != other.matcher) return false

return true
}

override fun hashCode(): Int {
var result = enabled.hashCode()
result = 31 * result + touched.hashCode()
result = 31 * result + priority
result = 31 * result + iconType.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + icon.hashCode()
result = 31 * result + matcher.hashCode()
return result
}

companion object {
private const val serialVersionUID: Long = -1L
private const val DEFAULT_COLOR = "808080"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2024 Elior "Mallowigi" Boukhobza
*
* 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.mallowigi.icons.associations

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.*
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import com.mallowigi.config.select.AtomSelectConfig
import com.mallowigi.models.IconType
import com.mallowigi.models.VirtualFileInfo
import java.io.DataInput
import java.io.DataOutput

class FileAssociationsIndex : FileBasedIndexExtension<String, RegexAssociation>() {
private val myIndexer: DataIndexer<String, RegexAssociation, FileContent> = FileAssociationsIndexer()

private val myValueExternalizer: DataExternalizer<RegexAssociation> = object : DataExternalizer<RegexAssociation> {
override fun save(out: DataOutput, value: RegexAssociation) {
out.writeBoolean(value.enabled)
out.writeInt(value.priority)
out.writeUTF(value.iconType.name)
out.writeUTF(value.name)
out.writeUTF(value.icon)
out.writeUTF(value.pattern)
}

override fun read(input: DataInput): RegexAssociation {
val association = RegexAssociation()
association.enabled = input.readBoolean()
association.priority = input.readInt()
association.iconType = IconType.valueOf(input.readUTF())
association.name = input.readUTF()
association.icon = input.readUTF()
association.pattern = input.readUTF()

return association
}
}

private val myInputFilter = FileBasedIndex.InputFilter { file: VirtualFile ->
file.isInLocalFileSystem
}

override fun getName(): ID<String, RegexAssociation> = NAME

override fun getInputFilter(): FileBasedIndex.InputFilter = myInputFilter

override fun dependsOnFileContent(): Boolean = false

override fun getIndexer(): DataIndexer<String, RegexAssociation, FileContent> = myIndexer

override fun getKeyDescriptor(): KeyDescriptor<String> = EnumeratorStringDescriptor.INSTANCE

override fun getValueExternalizer(): DataExternalizer<RegexAssociation> = myValueExternalizer

override fun getVersion(): Int = 1

internal class FileAssociationsIndexer : DataIndexer<String, RegexAssociation, FileContent> {
val fileAssociations = AtomSelectConfig.instance.selectedFileAssociations

override fun map(inputData: FileContent): MutableMap<String, RegexAssociation> {
val file = inputData.file
val path = file.path
val fileInfo = VirtualFileInfo(file)

val map = mutableMapOf<String, RegexAssociation>()
// Find association for the given path
val association = fileAssociations.findAssociation(fileInfo)
if (association != null && association is RegexAssociation) {
map[path] = association
}

return map
}
}

companion object {
val NAME = ID.create<String, RegexAssociation>("com.mallowigi.icons.associations.fileAssociationsIndex")
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2022 Elior "Mallowigi" Boukhobza
* Copyright (c) 2015-2024 Elior "Mallowigi" Boukhobza
*
* 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:
* 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 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.
* 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.
*/
@file:Suppress("HardCodedStringLiteral")

Expand All @@ -33,11 +33,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException

/**
* A Regex [Association]
*
* @constructor
*/
/** A Regex [Association]. */
@XStreamAlias("regex")
class RegexAssociation internal constructor() : Association() {
/** The regex pattern. */
Expand Down Expand Up @@ -90,6 +86,32 @@ class RegexAssociation internal constructor() : Association() {
pattern = other.matcher
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as RegexAssociation

if (enabled != other.enabled) return false
if (priority != other.priority) return false
if (iconType != other.iconType) return false
if (name != other.name) return false
if (icon != other.icon) return false
if (pattern != other.pattern) return false

return true
}

override fun hashCode(): Int {
var result = enabled.hashCode()
result = 31 * result + priority
result = 31 * result + iconType.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + icon.hashCode()
result = 31 * result + pattern.hashCode()
return result
}

companion object {
private val LOG = Logger.getInstance(RegexAssociation::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@
* 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.mallowigi.icons.providers

import com.intellij.ide.IconProvider
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiUtilCore
import com.intellij.util.indexing.FileBasedIndex
import com.mallowigi.icons.associations.Association
import com.mallowigi.icons.associations.Associations
import com.mallowigi.icons.associations.FileAssociationsIndex
import com.mallowigi.models.FileInfo
import com.mallowigi.models.IconType
import com.mallowigi.models.VirtualFileInfo
Expand All @@ -47,7 +51,7 @@ abstract class AbstractFileIconProvider : IconProvider(), DumbAware {
override fun getIcon(element: PsiElement, flags: Int): Icon? = when {
isNotApplicable() -> null
isOfType(element) -> findIcon(element)
else -> null
else -> null
}

/**
Expand All @@ -60,7 +64,7 @@ abstract class AbstractFileIconProvider : IconProvider(), DumbAware {
val virtualFile = PsiUtilCore.getVirtualFile(element)
return virtualFile?.let {
val file: FileInfo = VirtualFileInfo(it)
val association = findAssociation(file)
val association = findAssociation(file, virtualFile, element.project)
getIconForAssociation(association)
}
}
Expand All @@ -82,17 +86,21 @@ abstract class AbstractFileIconProvider : IconProvider(), DumbAware {
private fun loadIcon(association: Association): Icon? =
CacheIconProvider.instance.iconCache.getOrPut(association.icon) { getIcon(association.icon) }

/**
* Find association
*
* @param file
* @return
*/
private fun findAssociation(file: FileInfo): Association? = getSource().findAssociation(file)
/** Finds and retrieves the first matching association for the given file within the specified project scope. */
private fun findAssociation(file: FileInfo, virtualFile: VirtualFile, project: Project): Association? {
if (getType() == IconType.FOLDER) return getSource().findAssociation(file)

val fileBasedIndex = FileBasedIndex.getInstance()
val associations = fileBasedIndex.getValues(
FileAssociationsIndex.NAME,
file.path,
GlobalSearchScope.allScope(project)
)
return associations.firstOrNull()
}

/**
* Checks whether psiElement is of type (PsiFile/PsiDirectory) defined by
* this provider
* Checks whether psiElement is of type (PsiFile/PsiDirectory) defined by this provider
*
* @param element the psi element
* @return true if element is of type defined by this provider
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
order="first, after HiddenFolderIconProvider"
id="DefaultFolderIconProvider"/>

<fileBasedIndex implementation="com.mallowigi.icons.associations.FileAssociationsIndex"/>

<projectViewNodeDecorator implementation="com.mallowigi.tree.DefaultFoldersDecorator"/>
<projectViewNodeDecorator implementation="com.mallowigi.tree.DefaultFilesDecorator"/>
<projectViewNodeDecorator implementation="com.mallowigi.tree.HollowFoldersDecorator"/>
Expand Down

0 comments on commit c1e50a8

Please sign in to comment.