diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt index 81ae2b4..68dc2e0 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt @@ -3,17 +3,21 @@ package org.polyfrost.intelliprocessor.action import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil +import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import org.polyfrost.intelliprocessor.utils.* +import org.polyfrost.intelliprocessor.utils.PreprocessorVersion.Companion.preprocessorMainVersion import java.nio.file.Path import kotlin.io.path.relativeToOrNull -class PreprocessorFileJumpAction : DumbAwareAction() { +open class PreprocessorFileJumpAction : DumbAwareAction() { private companion object { private const val GROUP_ID = "Jump Failure" @@ -32,7 +36,7 @@ class PreprocessorFileJumpAction : DumbAwareAction() { val rootDirectory = findModuleDirForFile(currentPsiFile) ?.toPath() ?: return warning(project, "Could not find module directory for file") - val mainVersion = MainProject.get(currentPsiFile) + val mainVersion = currentPsiFile.preprocessorMainVersion ?: return warning(project, "Could not find mainProject. Is this a preprocessor project?") val currentlyEditingFile = currentPsiFile.virtualFile?.toNioPath() ?: return warning(project, "Could not find file on disk") @@ -55,8 +59,12 @@ class PreprocessorFileJumpAction : DumbAwareAction() { val ideView = LangDataKeys.IDE_VIEW.getData(e.dataContext) ?: return warning(project, "Could not find IDE view") - val caret = editor.caretModel.currentCaret.visualPosition - SourceSetFileDialog(project, targets) { selected -> + val caret = editor.caretModel.currentCaret.offset + + // For if the caret is inside a preprocessor conditional block, test each target version against the conditions + val foundConditionContext = testTargetsAgainstPreprocessorConditions(currentPsiFile, editor, targets) + + SourceSetFileDialog(project, targets, foundConditionContext) { selected -> val virtualFile = VfsUtil.findFile(rootDirectory.resolve(selected.toRelativePath()), true) if (virtualFile == null) { warning( @@ -76,13 +84,28 @@ class PreprocessorFileJumpAction : DumbAwareAction() { ideView.selectElement(psiFile) val newEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile) if (newEditor is TextEditor) { - newEditor.editor.caretModel.moveToVisualPosition(caret) + newEditor.editor.caretModel.moveToOffset(caret) + newEditor.editor.scrollingModel.scrollToCaret(ScrollType.CENTER) } else { warning(project, "Could not set cursor for non-text file") } }.show() } + // If the caret is inside a preprocessor conditional block, test each target version against the conditions there + private fun testTargetsAgainstPreprocessorConditions( + file: PsiFile, + editor: Editor, + targets: List + ): Boolean { + val selectedPos = editor.caretModel.currentCaret.offset + val conditions = PreprocessorConditions.findEnclosingConditionsOrNull(selectedPos, file) + if (conditions != null) { + targets.forEach { it.metOpeningCondition = conditions.testVersion(it.version) } + } + return conditions != null + } + private fun getSourceSetFrom(path: List, mainVersion: String, rootDirectory: Path): SourceSetFile? { if (path.size < 4) { return null diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginConfigurable.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginConfigurable.kt new file mode 100644 index 0000000..bd6c1fc --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginConfigurable.kt @@ -0,0 +1,125 @@ +package org.polyfrost.intelliprocessor.config + +import com.intellij.openapi.options.Configurable +import javax.swing.BoxLayout +import javax.swing.JCheckBox +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.border.EtchedBorder +import javax.swing.border.TitledBorder + +class PluginConfigurable : Configurable { + private lateinit var panel: JPanel + private lateinit var foldInactiveBlocksByDefaultCheckbox: JCheckBox + private lateinit var foldAllBlocksByDefaultCheckbox: JCheckBox + private lateinit var inspectionHighlightNonIndentedNestedIfsCheckbox: JCheckBox + private lateinit var inspectionHighlightCommentsNotMatchingIfIndentsCheckbox: JCheckBox + private lateinit var hideUnmatchedVersionsCheckbox: JCheckBox + private lateinit var addPreprocessorCommentOnEnterCheckbox: JCheckBox + + + override fun getDisplayName(): String = "IntelliProcessor" + + override fun createComponent(): JComponent { + + // Setup components + + fun J.tooltip(str: String): J = apply { toolTipText = str } + + foldInactiveBlocksByDefaultCheckbox = JCheckBox("Fold inactive preprocessor blocks by default") + .tooltip("Automatically folds preprocessor blocks that are conditionally inactive. (E.G. 'MC>=1.20' blocks in a 1.19 file)") + + foldAllBlocksByDefaultCheckbox = JCheckBox("Fold all preprocessor blocks by default").apply { + addChangeListener { event -> + // Disable the "fold inactive blocks" option if "fold all blocks" is enabled + foldInactiveBlocksByDefaultCheckbox.isEnabled = !(event.source as JCheckBox).isSelected + } + } + + inspectionHighlightNonIndentedNestedIfsCheckbox = + JCheckBox("Highlight non-indented nested \"if\" preprocessor directives (Code clarity)") + .tooltip( + "Highlights nested \"if\" preprocessor directives that are not indented more than their enclosing preprocessor block.\n" + + "\nThis does not break preprocessing, but can help improve code clarity by visually indicating the nested structure of preprocessor blocks." + ) + + inspectionHighlightCommentsNotMatchingIfIndentsCheckbox = + JCheckBox("Highlight preprocessor comments not matching their \"if\"'s indent (Code clarity)") + .tooltip( + "Highlights preprocessor comments whose indent does not match the indent of the corresponding \"if\" directive.\n" + + "\nThis does not break preprocessing, but can help improve code clarity by visually linking preprocessor comments to their corresponding \"if\" directives." + ) + + hideUnmatchedVersionsCheckbox = JCheckBox("Hide results that do not meet preprocessor conditions at the caret") + .tooltip("Hides version results in the 'Jump To Pre-Processed File' dialog that do not match the current file's preprocessor conditions found at the caret position.") + + addPreprocessorCommentOnEnterCheckbox = JCheckBox("Add preprocessor comment '//$$ ' automatically to new lines in a disabled preprocessor block") + .tooltip("When pressing Enter inside a disabled preprocessor block, automatically adds a preprocessor comment '//$$ ' to the new line.") + + // Arrange components + + fun titledBlock(str: String, block: JPanel.() -> Unit): JPanel = JPanel().apply { + border = TitledBorder(EtchedBorder(),str) + layout = BoxLayout(this, BoxLayout.Y_AXIS) + block() + } + + panel = JPanel() + + panel.apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + + add(titledBlock("Folding") { + add(foldAllBlocksByDefaultCheckbox) + add(foldInactiveBlocksByDefaultCheckbox) + }) + + add(titledBlock("Formatting") { + add(inspectionHighlightNonIndentedNestedIfsCheckbox) + add(inspectionHighlightCommentsNotMatchingIfIndentsCheckbox) + }) + + add(titledBlock("Jump To Pre-Processed File Action") { + add(hideUnmatchedVersionsCheckbox) + }) + + add(titledBlock("Misc") { + add(addPreprocessorCommentOnEnterCheckbox) + }) + + add(titledBlock("Info") { + add(JLabel("The keybinds can be configured from: Keymap > Plugins > IntelliProcessor")) + }) + } + + reset() + return panel + } + + override fun isModified(): Boolean = + foldAllBlocksByDefaultCheckbox.isSelected != PluginSettings.instance.foldAllBlocksByDefault + || foldInactiveBlocksByDefaultCheckbox.isSelected != PluginSettings.instance.foldInactiveBlocksByDefault + || inspectionHighlightNonIndentedNestedIfsCheckbox.isSelected != PluginSettings.instance.inspectionHighlightNonIndentedNestedIfs + || inspectionHighlightCommentsNotMatchingIfIndentsCheckbox.isSelected != PluginSettings.instance.inspectionHighlightCommentsNotMatchingIfIndents + || hideUnmatchedVersionsCheckbox.isSelected != PluginSettings.instance.hideUnmatchedVersions + || addPreprocessorCommentOnEnterCheckbox.isSelected != PluginSettings.instance.addPreprocessorCommentOnEnter + + override fun apply() { + PluginSettings.instance.foldAllBlocksByDefault = foldAllBlocksByDefaultCheckbox.isSelected + PluginSettings.instance.foldInactiveBlocksByDefault = foldInactiveBlocksByDefaultCheckbox.isSelected + PluginSettings.instance.inspectionHighlightNonIndentedNestedIfs = inspectionHighlightNonIndentedNestedIfsCheckbox.isSelected + PluginSettings.instance.inspectionHighlightCommentsNotMatchingIfIndents = inspectionHighlightCommentsNotMatchingIfIndentsCheckbox.isSelected + PluginSettings.instance.hideUnmatchedVersions = hideUnmatchedVersionsCheckbox.isSelected + PluginSettings.instance.addPreprocessorCommentOnEnter = addPreprocessorCommentOnEnterCheckbox.isSelected + } + + override fun reset() { + foldAllBlocksByDefaultCheckbox.isSelected = PluginSettings.instance.foldAllBlocksByDefault + foldInactiveBlocksByDefaultCheckbox.isSelected = PluginSettings.instance.foldInactiveBlocksByDefault + inspectionHighlightNonIndentedNestedIfsCheckbox.isSelected = PluginSettings.instance.inspectionHighlightNonIndentedNestedIfs + inspectionHighlightCommentsNotMatchingIfIndentsCheckbox.isSelected = PluginSettings.instance.inspectionHighlightCommentsNotMatchingIfIndents + hideUnmatchedVersionsCheckbox.isSelected = PluginSettings.instance.hideUnmatchedVersions + addPreprocessorCommentOnEnterCheckbox.isSelected = PluginSettings.instance.addPreprocessorCommentOnEnter + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginSettings.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginSettings.kt new file mode 100644 index 0000000..63746a9 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/config/PluginSettings.kt @@ -0,0 +1,41 @@ +package org.polyfrost.intelliprocessor.config + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@State(name = "IntelliProcessor", storages = [Storage("IntelliProcessor.xml")]) +@Service +class PluginSettings : PersistentStateComponent { + var foldAllBlocksByDefault: Boolean = false + var foldInactiveBlocksByDefault: Boolean = true + var inspectionHighlightNonIndentedNestedIfs: Boolean = true + var inspectionHighlightCommentsNotMatchingIfIndents: Boolean = true + var hideUnmatchedVersions: Boolean = false + var addPreprocessorCommentOnEnter = true + + override fun getState(): PluginSettings = this + + override fun loadState(state: PluginSettings) { + this.foldAllBlocksByDefault = state.foldAllBlocksByDefault + this.foldInactiveBlocksByDefault = state.foldInactiveBlocksByDefault + this.inspectionHighlightNonIndentedNestedIfs = state.inspectionHighlightNonIndentedNestedIfs + this.inspectionHighlightCommentsNotMatchingIfIndents = state.inspectionHighlightCommentsNotMatchingIfIndents + this.hideUnmatchedVersions = state.hideUnmatchedVersions + this.addPreprocessorCommentOnEnter = state.addPreprocessorCommentOnEnter + } + + companion object { + val instance: PluginSettings + get() = service() + + // Helper to modify settings and correctly persist changes + fun modify(action: PluginSettings.() -> Unit) { + val settings = PluginSettings().also { it.loadState(instance) } + settings.action() + instance.loadState(settings) // Pass changes back to instance via loadState() to persist + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorFolding.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorFolding.kt deleted file mode 100644 index 2718090..0000000 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorFolding.kt +++ /dev/null @@ -1,83 +0,0 @@ -package org.polyfrost.intelliprocessor.editor - -import com.intellij.lang.ASTNode -import com.intellij.lang.LanguageCommenters -import com.intellij.lang.folding.FoldingBuilderEx -import com.intellij.lang.folding.FoldingDescriptor -import com.intellij.openapi.editor.Document -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiComment -import com.intellij.psi.PsiElement -import com.intellij.psi.util.PsiTreeUtil - -class PreprocessorFolding : FoldingBuilderEx(), DumbAware { - - override fun getPlaceholderText(node: ASTNode): String { - val comment = node.psi as? PsiComment ?: return "..." - val directivePrefix = LanguageCommenters.INSTANCE.forLanguage(node.psi.language)?.lineCommentPrefix - ?: return "..." - - return comment.text.removePrefix(directivePrefix).trim() - } - - override fun buildFoldRegions( - root: PsiElement, - document: Document, - quick: Boolean, - ): Array { - val descriptors = mutableListOf() - val directivePrefix = (LanguageCommenters.INSTANCE.forLanguage(root.language).lineCommentPrefix ?: return emptyArray()) + "#" - - val allDirectives = PsiTreeUtil.findChildrenOfType(root, PsiComment::class.java) - .filter { it.text.startsWith(directivePrefix) } - - val stack = ArrayDeque() - - for (directive in allDirectives) { - val text = directive.text - - when { - text.startsWith(directivePrefix + "if") || text.startsWith(directivePrefix + "ifdef") -> { - stack.addLast(directive) - } - - text.startsWith(directivePrefix + "else") || text.startsWith(directivePrefix + "elseif") -> { - val startDirective = stack.removeLastOrNull() - if (startDirective != null) { - val commentLine = document.getLineNumber(directive.textOffset) - if (commentLine > 0) { - val prevLineEnd = document.getLineEndOffset(commentLine - 1) - descriptors.add( - FoldingDescriptor( - startDirective, - TextRange(startDirective.textRange.startOffset, prevLineEnd) - ) - ) - stack.addLast(directive) - } - } - } - - text.startsWith(directivePrefix + "endif") -> { - val startDirective = stack.removeLastOrNull() - if (startDirective != null) { - descriptors.add( - FoldingDescriptor( - startDirective, - TextRange(startDirective.textRange.startOffset, directive.textRange.endOffset) - ) - ) - } - } - } - } - - return descriptors.toTypedArray() - } - - override fun isCollapsedByDefault(node: ASTNode): Boolean { - return false - } - -} diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorNewLineHandler.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorNewLineHandler.kt index 49b73e7..5bb6f1c 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorNewLineHandler.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorNewLineHandler.kt @@ -8,11 +8,11 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.project.DumbAware import com.intellij.openapi.util.Ref -import com.intellij.psi.PsiComment -import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import org.polyfrost.intelliprocessor.ALLOWED_FILE_TYPES +import org.polyfrost.intelliprocessor.config.PluginSettings import org.polyfrost.intelliprocessor.utils.* +import org.polyfrost.intelliprocessor.utils.PreprocessorVersion.Companion.preprocessorVersion import java.util.Locale class PreprocessorNewLineHandler : EnterHandlerDelegateAdapter(), DumbAware { @@ -25,6 +25,11 @@ class PreprocessorNewLineHandler : EnterHandlerDelegateAdapter(), DumbAware { dataContext: DataContext, originalHandler: EditorActionHandler? ): Result { + + if (!PluginSettings.instance.addPreprocessorCommentOnEnter) { + return Result.Continue + } + val fileTypeName = EnterHandler.getLanguage(dataContext) ?.associatedFileType ?.name @@ -35,21 +40,11 @@ class PreprocessorNewLineHandler : EnterHandlerDelegateAdapter(), DumbAware { } val caretPos = caretOffset.get() - val psiAtOffset = file.findElementAt(caretPos) ?: return Result.Continue - val comment = psiAtOffset.containingComment ?: return Result.Continue - - val posInText = caretPos - comment.textRange.startOffset - if (posInText < 4) { - return Result.DefaultForceIndent - } - - val conditionals = findEnclosingConditionalBlock(comment) - if (conditionals.isEmpty()) { - return Result.Continue - } + val currentVersion = file.preprocessorVersion ?: return Result.Continue + val conditions = PreprocessorConditions.findEnclosingConditionsOrNull(caretPos, file) ?: return Result.Continue - val currentVersion = MainProject.comparable(file) - if (currentVersion != null && isInsideActiveBlock(conditionals, currentVersion)) { + // We are inside a preprocessor block, now check if it is active or not + if (conditions.testVersion(currentVersion)) { return Result.Continue } @@ -68,85 +63,4 @@ class PreprocessorNewLineHandler : EnterHandlerDelegateAdapter(), DumbAware { return Result.Stop } - private fun isInsideActiveBlock( - conditionals: List, - currentVersion: Int - ): Boolean { - for (directive in conditionals) { - when (directive) { - is PreprocessorDirective.If, is PreprocessorDirective.ElseIf -> { - val condition = (directive as ConditionContainingDirective).condition - if (evaluateCondition(condition, currentVersion)) { - return true - } - } - - is PreprocessorDirective.IfDef -> return true // TODO - is PreprocessorDirective.Else -> return true - is PreprocessorDirective.EndIf -> break - } - } - return false - } - - private fun evaluateCondition(condition: String, currentVersion: Int): Boolean { - if (!condition.startsWith("MC")) { - return true // Non-MC conditions are always considered "active" - } - - val match = Regex("""MC\s*(==|!=|<=|>=|<|>)\s*(\S+)""").find(condition) ?: return false - val (operator, rhsStr) = match.destructured - val rhs = Versions.makeComparable(rhsStr) ?: return false - - return when (operator) { - "==" -> currentVersion == rhs - "!=" -> currentVersion != rhs - "<=" -> currentVersion <= rhs - ">=" -> currentVersion >= rhs - "<" -> currentVersion < rhs - ">" -> currentVersion > rhs - else -> false - } - } - - private fun findEnclosingConditionalBlock(comment: PsiComment): List { - val block = mutableListOf() - var sibling: PsiElement? = comment - var nesting = 0 - - while (sibling != null) { - if (sibling is PsiComment) { - val directive = sibling.parseDirective() - if (directive == null) { - sibling = sibling.prevSibling - continue - } - - when (directive) { - is PreprocessorDirective.EndIf -> { - nesting++ - } - - is PreprocessorDirective.If, is PreprocessorDirective.IfDef -> { - if (nesting == 0) { - block.add(0, directive) - } else { - nesting-- - } - } - - is PreprocessorDirective.ElseIf, is PreprocessorDirective.Else -> { - if (nesting == 0) { - block.add(0, directive) - } - } - } - } - - sibling = sibling.prevSibling - } - - return block - } - } diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorSyntaxHighlight.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorSyntaxHighlight.kt index da1e7d2..6722047 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorSyntaxHighlight.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/PreprocessorSyntaxHighlight.kt @@ -21,6 +21,7 @@ import com.intellij.psi.PsiRecursiveElementWalkingVisitor import com.intellij.psi.impl.source.tree.PsiCommentImpl import org.polyfrost.intelliprocessor.ALLOWED_FILE_TYPES import org.polyfrost.intelliprocessor.Scope +import org.polyfrost.intelliprocessor.config.PluginSettings import java.util.ArrayDeque import java.util.Locale @@ -65,6 +66,11 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit private lateinit var commenter: Commenter private lateinit var highlighter: SyntaxHighlighter private var stack = ArrayDeque() + private var indentStack = ArrayDeque() + private var indentGetter: (PsiElement) -> Int = { 0 } + + private val doNestedIfIdentWarn get() = PluginSettings.instance.inspectionHighlightNonIndentedNestedIfs + private val doIdentMatchWarn get() = PluginSettings.instance.inspectionHighlightCommentsNotMatchingIfIndents override fun suitableForFile(file: PsiFile): Boolean { return file.fileType.name.uppercase(Locale.ROOT) in ALLOWED_FILE_TYPES @@ -84,6 +90,13 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit this.commenter = LanguageCommenters.INSTANCE.forLanguage(file.language) this.highlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(file.language, file.project, file.virtualFile) this.stack = ArrayDeque() + this.indentStack = ArrayDeque() + indentGetter = { + val offset = it.textOffset + val line = file.fileDocument.getLineNumber(offset) + val lineStart = file.fileDocument.getLineStartOffset(line) + offset - lineStart + } file.accept(object : PsiRecursiveElementWalkingVisitor() { override fun visitElement(element: PsiElement) { visit(element) @@ -112,6 +125,14 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit comment.startsWith("$$") -> { holder.add("$$".toDirectiveHighlight(element, prefixLength)) highlightCodeBlock(element, element.startOffset + prefixLength + 2, comment.drop(2)) + + if (doIdentMatchWarn) { + val indent = indentGetter(element) + val previousIndent = indentStack.peekFirst() + if (indent != (previousIndent ?: -1)) { + warn(element, "\"$$\" line is not indented the same as it's containing block (Code clarity)") + } + } } } } @@ -138,6 +159,21 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit stack.pop() } + if (doNestedIfIdentWarn || doIdentMatchWarn) { + val indent = indentGetter(element) + val previousIndent = indentStack.peekFirst() + if (directive == "if") { + if (doNestedIfIdentWarn && indent <= (previousIndent ?: -1)) { + warn(element, "\"$directive\" is not indented more than it's outer \"if\" block (Code clarity)") + } + indentStack.push(indent) + } else if (directive == "elseif") { + if (doIdentMatchWarn && indent != (previousIndent ?: -1)) { + warn(element, "\"$directive\" is not indented the same as it's starting \"if\" (Code clarity)") + } + } + } + stack.push(Scope.IF) holder.add(directive.toDirectiveHighlight(element, prefixLength)) @@ -165,6 +201,15 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit } private fun handleIfDef(element: PsiCommentImpl, segments: List, prefixLength: Int) { + if (doNestedIfIdentWarn) { + val indent = indentGetter(element) + val previousIndent = indentStack.peekFirst() + if (indent <= (previousIndent ?: -1)) { + warn(element, "\"ifdef\" is not indented more than it's outer \"if\" block (Code clarity)") + } + indentStack.push(indent) + } + stack.push(Scope.IF) holder.add("ifdef".toDirectiveHighlight(element, prefixLength)) @@ -189,6 +234,13 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit fail(element, "\"else\" must follow \"if\" (last in scope: ${previous?.name})") return } + if (doIdentMatchWarn) { + val indent = indentGetter(element) + val previousIndent = indentStack.peekFirst() + if (indent != (previousIndent ?: -1)) { + warn(element, "\"else\" is not indented the same as it's starting \"if\" (Code clarity)") + } + } stack.pop() stack.push(Scope.ELSE) @@ -205,6 +257,14 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit fail(element, "\"endif\" must follow \"if\" or \"else\" (last in scope: ${previous?.name})") return } + if (doIdentMatchWarn) { + val indent = indentGetter(element) + val previousIndent = indentStack.peekFirst() + if (indent != (previousIndent ?: -1)) { + warn(element, "\"endif\" is not indented the same as it's starting \"if\" (Code clarity)") + } + indentStack.pop() + } else if (doNestedIfIdentWarn) indentStack.pop() stack.pop() holder.add("endif".toDirectiveHighlight(element, prefixLength)) @@ -239,8 +299,14 @@ class PreprocessorSyntaxHighlight(private val project: Project) : HighlightVisit } } - private fun fail(element: PsiElement, message: String, eol: Boolean = false) { - val builder = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR) + private fun fail(element: PsiElement, message: String, eol: Boolean = false) = + highlightType(element, message, eol, HighlightInfoType.ERROR) + + private fun warn(element: PsiElement, message: String, eol: Boolean = false) = + highlightType(element, message, eol, HighlightInfoType.WEAK_WARNING) + + private fun highlightType(element: PsiElement, message: String, eol: Boolean = false, type: HighlightInfoType) { + val builder = HighlightInfo.newHighlightInfo(type) .descriptionAndTooltip(message) if (eol) { diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFolding.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFolding.kt new file mode 100644 index 0000000..f677526 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFolding.kt @@ -0,0 +1,128 @@ +package org.polyfrost.intelliprocessor.editor.folding + +import com.intellij.lang.ASTNode +import com.intellij.lang.LanguageCommenters +import com.intellij.lang.folding.FoldingBuilderEx +import com.intellij.lang.folding.FoldingDescriptor +import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import org.polyfrost.intelliprocessor.config.PluginSettings +import org.polyfrost.intelliprocessor.utils.PreprocessorConditions +import org.polyfrost.intelliprocessor.utils.PreprocessorVersion +import org.polyfrost.intelliprocessor.utils.PreprocessorVersion.Companion.preprocessorVersion +import org.polyfrost.intelliprocessor.utils.allPreprocessorDirectiveComments +import org.polyfrost.intelliprocessor.utils.directivePrefix + +/** + * Registering the folding builder class only allows 1 class per language, so we will make this abstract and extend it for java and kotlin + */ +abstract class PreprocessorFolding : FoldingBuilderEx(), DumbAware { + + override fun getPlaceholderText(node: ASTNode): String { + val comment = node.psi as? PsiComment ?: return "..." + val directivePrefix = LanguageCommenters.INSTANCE.forLanguage(node.psi.language)?.lineCommentPrefix + ?: return "..." + + return comment.text.removePrefix(directivePrefix).trim() + } + + override fun buildFoldRegions( + root: PsiElement, + document: Document, + quick: Boolean, + ): Array { + val descriptors = mutableListOf() + + // Required to allow the "fold inactive blocks by default" feature + // Disabled for quick mode so we don't run all the condition checks on the fly + val preprocessorVersion = + if (!PluginSettings.Companion.instance.foldInactiveBlocksByDefault) null + else root.containingFile.preprocessorVersion + + val directivePrefix = root.directivePrefix() + val allDirectives = root.containingFile.allPreprocessorDirectiveComments() + val stack = ArrayDeque() + + for (directive in allDirectives) { + val text = directive.text + + when { + text.startsWith(directivePrefix + "if") || text.startsWith(directivePrefix + "ifdef") -> { + stack.addLast(directive) + } + + text.startsWith(directivePrefix + "else") -> { // elseif caught too + val startDirective = stack.removeLastOrNull() + if (startDirective != null) { + val commentLine = document.getLineNumber(directive.textOffset) + if (commentLine > 0) { + val prevLineEnd = document.getLineEndOffset(commentLine - 1) + descriptors.add( + fold( + startDirective, + startDirective.textRange.startOffset, + prevLineEnd, + preprocessorVersion, + allDirectives + ) + ) + stack.addLast(directive) + } + } + } + + text.startsWith(directivePrefix + "endif") -> { + val startDirective = stack.removeLastOrNull() + if (startDirective != null) { + descriptors.add( + fold( + startDirective, + startDirective.textRange.startOffset, + directive.textRange.endOffset, + preprocessorVersion, + allDirectives + ) + ) + } + } + } + } + + return descriptors.toTypedArray() + } + + private fun fold( + element: PsiComment, + startOffset: Int, + endOffset: Int, + thisVersion: PreprocessorVersion?, + allDirectives: List + ): FoldingDescriptor { + + if (thisVersion == null || PluginSettings.Companion.instance.foldAllBlocksByDefault) { + return FoldingDescriptor(element, TextRange(startOffset, endOffset)) + } + + val shouldFold = PreprocessorConditions.Companion.findEnclosingConditionsOrNull(element, allDirectives)?.let { + !it.testVersion(thisVersion) + } + + return FoldingDescriptor( + element.node, + TextRange(startOffset, endOffset), + null, + emptySet(), + false, + null, + shouldFold + ) + } + + override fun isCollapsedByDefault(node: ASTNode): Boolean { + return PluginSettings.Companion.instance.foldAllBlocksByDefault + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingJava.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingJava.kt new file mode 100644 index 0000000..f011a29 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingJava.kt @@ -0,0 +1,3 @@ +package org.polyfrost.intelliprocessor.editor.folding + +class PreprocessorFoldingJava : PreprocessorFolding() \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingKotlin.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingKotlin.kt new file mode 100644 index 0000000..b851ce8 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/editor/folding/PreprocessorFoldingKotlin.kt @@ -0,0 +1,3 @@ +package org.polyfrost.intelliprocessor.editor.folding + +class PreprocessorFoldingKotlin : PreprocessorFolding() \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/MainProject.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/MainProject.kt deleted file mode 100644 index 37f4160..0000000 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/MainProject.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.polyfrost.intelliprocessor.utils - -import com.intellij.psi.PsiFile -import java.nio.file.Files - -object MainProject { - - fun get(file: PsiFile): String? { - val moduleDir = findModuleDirForFile(file) ?: run { - println("Module directory could not be found for file: ${file.virtualFile?.path}") - return null - } - - val versionFile = moduleDir.toPath().resolve("versions/mainProject") - if (!Files.exists(versionFile)) { - println("Main project version file does not exist at: $versionFile") - return null - } - - return Files.readString(versionFile).trim() - } - - fun comparable(file: PsiFile): Int? { - val version = get(file) ?: return null - return Versions.makeComparable(version) - } - -} diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorConditions.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorConditions.kt new file mode 100644 index 0000000..8f47f18 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorConditions.kt @@ -0,0 +1,183 @@ +package org.polyfrost.intelliprocessor.utils + +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiFile +import com.intellij.psi.util.startOffset + + +class PreprocessorConditions private constructor( + private val trueConditions: List, + private val falseConditions: List +) { + // Unknown / Unresolvable expressions will evaluate as null, and return failureResult + fun testVersion(version: PreprocessorVersion, failureResult: Boolean = true): Boolean { + // Check all conditions that should be true + for (directive in trueConditions) { + when (directive) { + is PreprocessorDirective.If, is PreprocessorDirective.ElseIf -> { + val condition = (directive as ConditionContainingDirective).condition + if (!(evaluateBooleanConditions(condition, version) ?: return failureResult)) { + return false + } + } + is PreprocessorDirective.IfDef -> continue // TODO + is PreprocessorDirective.Else -> continue + is PreprocessorDirective.EndIf -> break + } + } + + // Then check all conditions that should be false + for (directive in falseConditions) { + when (directive) { + is PreprocessorDirective.If, is PreprocessorDirective.ElseIf -> { + val condition = (directive as ConditionContainingDirective).condition + if (evaluateBooleanConditions(condition, version) ?: return failureResult) { + return false + } + } + is PreprocessorDirective.IfDef -> return false // TODO + is PreprocessorDirective.Else -> return false // Shouldn't ever occur + is PreprocessorDirective.EndIf -> break + } + } + + return true + } + + companion object { + + // Only providing file and an offset point within it, we find the nearest preceding directive comment and use that as the starting point + fun findEnclosingConditionsOrNull(offset: Int, file: PsiFile): PreprocessorConditions? { + val directives = file.allPreprocessorDirectiveComments() + val previousComment = directives.lastOrNull { it.startOffset <= offset } ?: return null + return findEnclosingConditionsOrNull(previousComment, directives) + } + + fun findEnclosingConditionsOrNull( + comment: PsiComment, // This comment should only be an already identified directive comment + allDirectives: List + ): PreprocessorConditions? { + var index = allDirectives.indexOfFirst { it === comment } + if (index == -1) return null + + val trueBlock = mutableListOf() + val falseBlock = mutableListOf() + var sibling: PsiComment? = comment + var nesting = 0 + + // Tracks whether our reverse iteration has passed an else/elseif directive + // If so, we add further directives to the falseBlock instead of the trueBlock, until we leave the initial if block + var elseNesting = false + + fun prev(): PsiComment? = allDirectives.getOrNull(--index) + + while (sibling != null) { + val directive = sibling.parseDirective() + if (directive == null) { + sibling = prev() + continue + } + + when (directive) { + is PreprocessorDirective.EndIf -> { + nesting++ + } + + is PreprocessorDirective.If, is PreprocessorDirective.IfDef -> { + if (nesting == 0) { + if (elseNesting) { + falseBlock.add(0, directive) + } else { + trueBlock.add(0, directive) + } + elseNesting = false + } else { + nesting-- + } + } + + is PreprocessorDirective.ElseIf, is PreprocessorDirective.Else -> { + if (nesting == 0) { + if (elseNesting) { + falseBlock.add(0, directive) + } else { + trueBlock.add(0, directive) + } + elseNesting = true + } + } + } + sibling = prev() + } + + if (trueBlock.isEmpty() && falseBlock.isEmpty()) return null + + return PreprocessorConditions(trueBlock, falseBlock) + } + + private fun logAndNull(str: String): Boolean? { + println(str) + return null + } + + private val BOOLEAN_SPLITTER = Regex("""\s*(&&|\|\||[^&|]+)\s*""") + + private fun evaluateBooleanConditions(conditions: String, currentVersion: PreprocessorVersion): Boolean? { + // Multiple conditions separated by && or || + if (conditions.contains("||") || conditions.contains("&&")) { + val tokens = BOOLEAN_SPLITTER.findAll(conditions).map { it.groupValues[1] }.toList() + var result = evaluateCondition(tokens[0], currentVersion) + ?: return logAndNull("Could not evaluate condition: ${tokens[0]}") + var i = 1 + while (i < tokens.size) { + val op = tokens[i] + val next = evaluateCondition(tokens[i + 1], currentVersion) + ?: return logAndNull("Could not evaluate condition: ${tokens[i + 1]}") + result = when (op) { + "&&" -> result && next + "||" -> result || next + else -> return logAndNull("op wasn't && or || in: $conditions") // Shouldn't occur + } + i += 2 + } + return result + } + + // Single condition + return evaluateCondition(conditions, currentVersion) + } + + private fun evaluateCondition(conditionRaw: String, currentVersion: PreprocessorVersion): Boolean? { + val condition = conditionRaw.trim() + if (!condition.startsWith("MC")) { + + // Check simple loader conditions + val lower = condition.lowercase().removePrefix("!") + if (lower.contains("fabric") || lower.contains("forge")) { + return condition.startsWith("!") != (currentVersion.loader == lower) + } + + return logAndNull("Could not evaluate unknown condition: $condition") // Unknown conditions are always considered "active" + } + + val match = Regex("""MC\s*(==|!=|<=|>=|<|>)\s*(\S+)""").find(condition) + ?: return logAndNull("Could not evaluate (MC ?? Number) condition: $condition") + val (operator, rhsStr) = match.destructured + + + val rhs = rhsStr.toIntOrNull() + ?: return logAndNull("Could not evaluate version number in MC condition: $condition") + + val compare = currentVersion.mc + return when (operator) { + "==" -> compare == rhs + "!=" -> compare != rhs + "<=" -> compare <= rhs + ">=" -> compare >= rhs + "<" -> compare < rhs + ">" -> compare > rhs + else -> logAndNull("Could not evaluate MC condition operator: $condition") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorVersion.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorVersion.kt new file mode 100644 index 0000000..bdcbced --- /dev/null +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/PreprocessorVersion.kt @@ -0,0 +1,68 @@ +package org.polyfrost.intelliprocessor.utils + +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.toNioPathOrNull +import com.intellij.psi.PsiFile +import java.nio.file.Files +import kotlin.collections.toList +import kotlin.io.path.relativeTo + +// Represents a preprocessor comparable minecraft version along with its loader (e.g., fabric, forge). +// Accessed as properties on string-ified preprocessor versions and PsiFiles. +class PreprocessorVersion private constructor(val mc: Int, val loader: String) { + companion object { + val NULL = PreprocessorVersion(0, "null") + + val String.preprocessorVersion: PreprocessorVersion? get() { + val int = makeComparable(this) ?: return null + val loader = split("-").getOrNull(1) ?: return null + return PreprocessorVersion(int, loader) + } + + val PsiFile.preprocessorVersion: PreprocessorVersion? get() { + val version = versionStringOfFile ?: return null + return version.preprocessorVersion + } + + private val extractFromModule = """\b\d+\.\d+(?:\.\d+)?-\w+\b""".toRegex() + val PsiFile.versionStringOfFile: String? get() { + val vf: VirtualFile? = virtualFile + if (vf == null) { // No backing file + val module = ModuleUtilCore.findModuleForPsiElement(this) ?: return null + return extractFromModule.find(module.name)?.value ?: preprocessorMainVersion + } + val rootDirectory = findModuleDirForFile(this)?.toPath() ?: return null + val relPath = vf.toNioPathOrNull()?.relativeTo(rootDirectory)?.toList() ?: return null + if (relPath[0].toString() != "versions") return preprocessorMainVersion + return relPath[1].toString() + } + + val PsiFile.preprocessorMainVersion: String? get() { + val moduleDir = findModuleDirForFile(this) ?: run { + println("Module directory could not be found for file: ${virtualFile?.path}") + return null + } + + val versionFile = moduleDir.toPath().resolve("versions/mainProject") + if (!Files.exists(versionFile)) { + println("Main project version file does not exist at: $versionFile") + return null + } + + return Files.readString(versionFile).trim() + } + + private val regex = "(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?".toRegex() + private fun makeComparable(version: String): Int? { + val match = regex.find(version) ?: return null + val groups = match.groups + + val major = groups["major"]?.value?.toInt() ?: return null + val minor = groups["minor"]?.value?.toInt() ?: return null + val patch = groups["patch"]?.value?.toInt() ?: 0 + + return major * 10000 + minor * 100 + patch + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt index 9f14ae1..0590ecf 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt @@ -1,5 +1,6 @@ package org.polyfrost.intelliprocessor.utils +import org.polyfrost.intelliprocessor.utils.PreprocessorVersion.Companion.preprocessorVersion import java.nio.file.Path data class SourceSetFile( @@ -28,19 +29,11 @@ data class SourceSetFile( } } + var metOpeningCondition = true val displayVersion = subVersion ?: mainVersion - // Used to sort entries as 1.8.9 will order before 1.12.2 otherwise - val versionInt = displayVersion.split('-').let { platform -> - // Convert semantic version to the preprocessor int: 1.21.2 -> 12102 - fun List.getOrZero(index: Int) = getOrNull(index)?.toIntOrNull() ?: 0 - val semVer = platform[0].split('.') - semVer.getOrZero(0) * 10000 + semVer.getOrZero(1) * 100 + semVer.getOrZero(2) - } - - // Simpler search key used to streamline keyboard navigation via search, 1.21.2-fabric -> 12102fabric - private val simpleVersion = "$versionInt${displayVersion.split('-')[1]}" + val version = displayVersion.preprocessorVersion ?: PreprocessorVersion.NULL override fun toString(): String { val displayFile = classPath.last() @@ -49,7 +42,7 @@ data class SourceSetFile( else "" // e.g. [12102fabric] | 1.21.2-fabric | MyClass.java (main) - return "[$simpleVersion] | $displayVersion | $displayFile $srcMark" + return "[${version.mc}${version.loader}] | $displayVersion | $displayFile $srcMark" } } diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index a0850f6..433e723 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -6,11 +6,16 @@ import com.intellij.ui.CollectionListModel import com.intellij.ui.DocumentAdapter import com.intellij.ui.JBColor import com.intellij.ui.SearchTextField +import com.intellij.ui.components.CheckBox import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane +import org.polyfrost.intelliprocessor.config.PluginSettings import java.awt.BorderLayout +import java.awt.Component import java.awt.Font import java.awt.GridLayout +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.DefaultListCellRenderer @@ -24,10 +29,12 @@ import javax.swing.event.DocumentEvent class SourceSetFileDialog( project: Project, sourceFilesUnsorted: List, - private val onFileChosen: (SourceSetFile) -> Unit + private val hadConditions: Boolean, + private val onFileChosen: (SourceSetFile) -> Unit, ) : DialogWrapper(project) { - private val sourceFiles = sourceFilesUnsorted.sortedBy { it.versionInt } + // Sort entries via int version as 1.8.9 will order before 1.12.2 otherwise + private val sourceFiles = sourceFilesUnsorted.sortedBy { it.version.mc } private val listModel = CollectionListModel(sourceFiles) private val list = JBList(listModel).apply { @@ -42,8 +49,13 @@ class SourceSetFileDialog( cellHasFocus ).apply { // Further differentiate preprocessed generated files with font style - if ((value as SourceSetFile).isNonGenerated) { - font = font.deriveFont(Font.BOLD) + (value as? SourceSetFile)?.let { + if (it.isNonGenerated) { + font = font.deriveFont(Font.BOLD) + } + if (!it.metOpeningCondition) { + foreground = JBColor.GRAY + } } } @@ -74,7 +86,21 @@ class SourceSetFileDialog( } }) } - private val search = SearchTextField() + + private val search = SearchTextField().apply { + // Keyboard navigation: Down arrow focuses into the list from the search field + textEditor.addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent?) { + if (e?.keyCode == KeyEvent.VK_DOWN || e?.keyCode == KeyEvent.VK_KP_DOWN) { + if (list.selectedValue == null) { + list.selectedIndex = 0 + } + list.requestFocusInWindow() + e.consume() + } + } + }) + } init { title = "Select Preprocessed Source File" @@ -89,29 +115,71 @@ class SourceSetFileDialog( search.addDocumentListener(object : DocumentAdapter() { override fun textChanged(e: DocumentEvent) { - val filter = search.text.lowercase() - listModel.replaceAll(sourceFiles.filter { - it.toString().lowercase().contains(filter) - }) - - if (filter.isEmpty() || listModel.isEmpty) { - list.setSelectedValue(null, false) - } else { - // Improve keyboard navigation by auto-selecting the first result - list.setSelectedValue(listModel.getElementAt(0), false) - } + filterList() } }) panel.add(search, BorderLayout.NORTH) - panel.add(JLabel(" (override) files are, non-generated, override files present in the versions//src/ directory.").apply { - font = font.deriveFont(Font.ITALIC, 12f) - foreground = JBColor.GRAY - }, BorderLayout.CENTER) - panel.add(JBScrollPane(list), BorderLayout.SOUTH) + panel.add(JBScrollPane(list), BorderLayout.CENTER) + bottomPanelOrNull()?.let { + panel.add(it, BorderLayout.SOUTH) + } return panel } + private fun bottomPanelOrNull(): JPanel? { + fun String.label(): JLabel = JLabel(this).apply { + font = font.deriveFont(Font.ITALIC, 12f) + foreground = JBColor.GRAY + } + + val belowList = mutableListOf() + if (sourceFiles.any { it.subVersion != null && it.isNonGenerated }) { + belowList.add(" - (override) files are, non-generated, override files present in the versions//src/ directory.".label()) + } + if (!hadConditions) { + belowList.add(" - No preprocessor conditions were found to apply at the caret position to test results with. Showing all.".label()) + } else if (sourceFiles.any { !it.metOpeningCondition }) { + val hide = PluginSettings.instance.hideUnmatchedVersions + val hideText = + " - Results that do not meet the preprocessor conditions at the caret position have been hidden." + val showText = + " - Faded results are those that do not meet the preprocessor conditions at the caret position." + val label = (if (hide) hideText else showText).label() + belowList.add(label) + belowList.add(CheckBox("Hide results that do not meet preprocessor conditions at caret", hide).apply { + addActionListener { + PluginSettings.modify { hideUnmatchedVersions = isSelected } + label.text = if (isSelected) hideText else showText + filterList() + } + }) + filterList() + } + + return if (belowList.isEmpty()) null else JPanel(GridLayout(belowList.size, 1)).apply { + for (below in belowList) { + add(below, BorderLayout.NORTH) + } + } + } + + private fun filterList() { + val filter = search.text.lowercase() + listModel.replaceAll(sourceFiles.filter { + it.toString().lowercase().contains(filter) + // Hide entries that do not meet the opening condition if the setting is enabled + && (!PluginSettings.instance.hideUnmatchedVersions || it.metOpeningCondition) + }) + + if (filter.isEmpty() || listModel.isEmpty) { + list.setSelectedValue(null, false) + } else { + // Improve keyboard navigation by auto-selecting the first result + list.setSelectedValue(listModel.getElementAt(0), false) + } + } + override fun doOKAction() { val selected = list.selectedValue ?: return onFileChosen(selected) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/Versions.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/Versions.kt deleted file mode 100644 index 9a6556a..0000000 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/Versions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.polyfrost.intelliprocessor.utils - -object Versions { - - private val regex = "(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?".toRegex() - - fun makeComparable(version: String): Int? { - val match = regex.find(version) ?: return null - val groups = match.groups - - val major = groups["major"]?.value?.toInt() ?: return null - val minor = groups["minor"]?.value?.toInt() ?: return null - val patch = groups["patch"]?.value?.toInt() ?: 0 - - return major * 10000 + minor * 100 + patch - } - -} diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/utils.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/utils.kt index 70d7b2a..3ab6de3 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/utils.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/utils.kt @@ -1,30 +1,20 @@ package org.polyfrost.intelliprocessor.utils import com.intellij.codeInsight.completion.* +import com.intellij.lang.LanguageCommenters import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.patterns.ElementPattern import com.intellij.psi.* -import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.util.PsiTreeUtil import com.intellij.util.ProcessingContext import java.nio.file.Path val Editor.activeFile: PsiFile? get() = project?.let { project -> PsiDocumentManager.getInstance(project).getPsiFile(this.document) } -val PsiElement.containingComment: PsiComment? - get() = when (this) { - is PsiComment -> this - is PsiWhiteSpace, is PsiPlainText, is LeafPsiElement -> { - this.prevSibling?.takeIf { it is PsiComment } as? PsiComment - ?: this.parent?.takeIf { it is PsiComment } as? PsiComment - } - - else -> parent as? PsiComment - } - fun Iterable.joinToPath(): Path { return reduce { acc, path -> acc.resolve(path) @@ -52,3 +42,13 @@ fun warning( .createNotification(content, NotificationType.WARNING) .notify(project) } + +fun PsiElement.directivePrefix(): String? { + return (LanguageCommenters.INSTANCE.forLanguage(language).lineCommentPrefix ?: return null) + "#" + } + +fun PsiFile.allPreprocessorDirectiveComments(): List { + val directivePrefix = directivePrefix() ?: return emptyList() + return PsiTreeUtil.findChildrenOfType(this, PsiComment::class.java) + .filter { it.text.startsWith(directivePrefix) } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/java-plugin.xml b/src/main/resources/META-INF/java-plugin.xml index 9948ecd..71f7a37 100644 --- a/src/main/resources/META-INF/java-plugin.xml +++ b/src/main/resources/META-INF/java-plugin.xml @@ -2,7 +2,7 @@ diff --git a/src/main/resources/META-INF/kotlin-plugin.xml b/src/main/resources/META-INF/kotlin-plugin.xml index 6e8e2d9..2affa36 100644 --- a/src/main/resources/META-INF/kotlin-plugin.xml +++ b/src/main/resources/META-INF/kotlin-plugin.xml @@ -1,13 +1,8 @@ - - - - - diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 099a2a8..9f42a73 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -3,6 +3,11 @@ IntelliProcessor Polyfrost + + + + + com.intellij.modules.platform com.intellij.modules.java org.jetbrains.kotlin @@ -15,6 +20,8 @@ + +