Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fold Labeled Statements #195

Open
akefirad opened this issue Jan 3, 2025 · 4 comments
Open

Fold Labeled Statements #195

akefirad opened this issue Jan 3, 2025 · 4 comments

Comments

@akefirad
Copy link

akefirad commented Jan 3, 2025

Hi there,

Objective: to be able to fold blocks in a Spock test (e.g. given, when, then, etc) for more readability and better code navigation in the IDE

For example, having something like this:
now
I'd like to be able to make it like this:
wanted

I wanted to implement this as a plugin, but it seemed to be an overkill and was hoping if LivePlugin can help with this.

I tried to find something in the repo and docs regarding folding, but couldn't find anything. Does LivePlugin support such functionalities, more specifically FoldingBuilder?

I never developed plugins so might not be super helpful, but happy to contribute in any way I can.
Cheers.

@dkandalov
Copy link
Owner

Hi,

LivePlugin is just compiling and loading classes into the JVM at runtime. It has a thin wrapper around some of the IDE APIs but fundamentally LivePlugin doesn't restrict you from doing anything (except that some IntelliJ APIs are not necessarily designed to be reloaded at runtime).

Here is an example of FoldingBuilder that I've been using for the last couple years to fold Kotlin code https://gist.github.com/dkandalov/82f37b0d3a6f8b3e4c6f1f2296a63e41

If you want something a bit more basic, here is an older collapse java keywords into symbols action https://gist.github.com/dkandalov/5553999

@akefirad
Copy link
Author

akefirad commented Jan 5, 2025

This is awesome. Thank you.
I tried something quick based on what you said:

import com.intellij.lang.ASTNode
import com.intellij.lang.LanguageExtensionPoint
import com.intellij.lang.folding.CustomFoldingBuilder
import com.intellij.lang.folding.FoldingBuilder
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.lang.folding.LanguageFolding
import com.intellij.openapi.editor.Document
import com.intellij.openapi.extensions.DefaultPluginDescriptor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.elementType
import com.intellij.util.KeyedLazyInstance


val pluginDescriptor =
    DefaultPluginDescriptor(PluginId.getId("LivePlugin"), SpockFoldingBuilder::class.java.classLoader)
val extensionPoint: KeyedLazyInstance<FoldingBuilder> =
    LanguageExtensionPoint("Groovy", "Plugin\$SpockFoldingBuilder", pluginDescriptor)
LanguageFolding.EP_NAME.point.registerExtension(extensionPoint, pluginDisposable)

class SpockFoldingBuilder : CustomFoldingBuilder(), DumbAware {

    private val labels = setOf("and:", "expect:", "given:", "then:", "when:", "where:")

    override fun buildLanguageFoldRegions(
        descriptors: MutableList<FoldingDescriptor>,
        root: PsiElement,
        document: Document,
        quick: Boolean,
    ) {
        if (root.elementType?.language?.id != "Groovy") return
        addSpockLabelFoldRegions(descriptors, root)
    }

    private fun addSpockLabelFoldRegions(descriptors: MutableList<FoldingDescriptor>, e: PsiElement) {
        for (child in e.children) {
            if (child.isSpockLabel())
                addSpockLabelFoldRegion(descriptors, child)

            addSpockLabelFoldRegions(descriptors, child)
        }
    }

    private fun addSpockLabelFoldRegion(descriptors: MutableList<FoldingDescriptor>, e: PsiElement) {
        var next = e.nextSibling

        val start = e.textRange.startOffset
        var end = e.textRange.endOffset

        while (next != null && !(next.isSpockLabel())) {
            if (!next.isWhiteSpaceOrNewLine())
                end = next.textRange.endOffset

            next = next.nextSibling
        }

        if (end > start) {
            val range = TextRange(start, end)
            descriptors.add(FoldingDescriptor(e.node, range))
        }
    }

    @Suppress("UnstableApiUsage")
    private fun PsiElement.isWhiteSpaceOrNewLine() =
        this is PsiWhiteSpace || node.elementType.debugName == "new line"

    @Suppress("UnstableApiUsage")
    private fun PsiElement.isSpockLabel(): Boolean = this.elementType?.debugName == "LABELED_STATEMENT" &&
        labels.stream().anyMatch { text.startsWith(it) }

    override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange) = node.text

    override fun isRegionCollapsedByDefault(node: ASTNode) = false
}

But unfortunately it doesn't seem to work. I'm not sure what the issue is, but the above works just fine in a proper plugin project.
Do you know what could the issue? Or maybe another question, is there a way to debug scripts in LivePlugin? (a break-point or stdout?)

Thanks.

@dkandalov
Copy link
Owner

I tried SpockFoldingBuilder above and it worked fine for me.
Maybe for some reason the code editor didn't refresh folding regions 🤷

To debug you can show notifications or print to stdout (which ends up in the IDE log file). For example, see https://github.com/dkandalov/live-plugin/blob/master/plugin-examples/kotlin/hello-world/plugin.kts

Not sure if breakpoints are possible because it will be JVM debugging itself.

@akefirad
Copy link
Author

akefirad commented Jan 6, 2025

You’re right. Now it works. Not sure what happened. Thanks for the support.
Great work BTW. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants