Skip to content

PSI Structure

This document describes the PSI (Program Structure Interface) implementation for BerryCrush files.

Overview

PSI is the IntelliJ Platform's way of representing code structure. It provides: - A tree structure representing file contents - Navigation between elements - Modification support - Integration with IDE features

Element Hierarchy

BerryCrushFile (PsiFile)
├── BerryCrushFeatureElement
│   └── name: String
├── BerryCrushScenarioElement
│   ├── name: String
│   └── steps: List<BerryCrushStepElement>
├── BerryCrushScenarioOutlineElement
│   ├── name: String
│   ├── steps: List<BerryCrushStepElement>
│   └── examples: BerryCrushExamplesElement
├── BerryCrushFragmentElement
│   ├── name: String
│   └── steps: List<BerryCrushStepElement>
└── BerryCrushBackgroundElement
    └── steps: List<BerryCrushStepElement>

Core Elements

BerryCrushFile

The root element representing a .scenario or .fragment file.

class BerryCrushFile(viewProvider: FileViewProvider) : 
    PsiFileBase(viewProvider, BerryCrushLanguage) {

    fun getFeature(): BerryCrushFeatureElement?
    fun getScenarios(): List<BerryCrushScenarioElement>
    fun getFragments(): List<BerryCrushFragmentElement>
}

BerryCrushFeatureElement

Represents a Feature: declaration.

interface BerryCrushFeatureElement : BerryCrushNamedElement {
    fun getKeyword(): PsiElement
    fun getDescription(): String?
    fun getTags(): List<BerryCrushTagElement>
}

BerryCrushScenarioElement

Represents a Scenario: block.

interface BerryCrushScenarioElement : BerryCrushNamedElement {
    fun getKeyword(): PsiElement
    fun getSteps(): List<BerryCrushStepElement>
    fun getTags(): List<BerryCrushTagElement>
}

BerryCrushFragmentElement

Represents a Fragment: definition.

interface BerryCrushFragmentElement : BerryCrushNamedElement, PsiNameIdentifierOwner {
    fun getKeyword(): PsiElement
    fun getSteps(): List<BerryCrushStepElement>

    override fun getName(): String?
    override fun setName(name: String): PsiElement
    override fun getNameIdentifier(): PsiElement?
}

BerryCrushStepElement

Represents a step (Given/When/Then/And/But).

interface BerryCrushStepElement : BerryCrushElement {
    fun getKeyword(): BerryCrushKeyword
    fun getText(): String
    fun getOperationRef(): BerryCrushOperationRefElement?
    fun getDataTable(): BerryCrushDataTableElement?
    fun getDocString(): BerryCrushDocStringElement?
}

BerryCrushIncludeElement

Represents an include directive.

interface BerryCrushIncludeElement : BerryCrushElement {
    fun getFragmentName(): String
    fun getReference(): PsiReference
    fun resolve(): BerryCrushFragmentElement?
}

Named Elements

Elements that can be renamed implement PsiNameIdentifierOwner:

interface BerryCrushNamedElement : PsiNameIdentifierOwner, NavigatablePsiElement {
    override fun getName(): String?
    override fun setName(name: String): PsiElement
    override fun getNameIdentifier(): PsiElement?
}

Implementation Example

class BerryCrushFragmentElementImpl(node: ASTNode) : 
    BerryCrushElementImpl(node), BerryCrushFragmentElement {

    override fun getName(): String? {
        return nameIdentifier?.text
    }

    override fun setName(name: String): PsiElement {
        nameIdentifier?.let { identifier ->
            val newElement = BerryCrushElementFactory.createIdentifier(project, name)
            identifier.replace(newElement)
        }
        return this
    }

    override fun getNameIdentifier(): PsiElement? {
        return findChildByType(BerryCrushTypes.TEXT)
    }
}

Element Types

Token Types

Defined by the lexer:

object BerryCrushTypes {
    // Keywords
    val FEATURE = IElementType("FEATURE", BerryCrushLanguage)
    val SCENARIO = IElementType("SCENARIO", BerryCrushLanguage)
    val FRAGMENT = IElementType("FRAGMENT", BerryCrushLanguage)
    val GIVEN = IElementType("GIVEN", BerryCrushLanguage)
    val WHEN = IElementType("WHEN", BerryCrushLanguage)
    val THEN = IElementType("THEN", BerryCrushLanguage)
    val AND = IElementType("AND", BerryCrushLanguage)
    val BUT = IElementType("BUT", BerryCrushLanguage)
    val INCLUDE = IElementType("INCLUDE", BerryCrushLanguage)

    // Content
    val TEXT = IElementType("TEXT", BerryCrushLanguage)
    val COMMENT = IElementType("COMMENT", BerryCrushLanguage)
    val OP_REF = IElementType("OP_REF", BerryCrushLanguage)
    val VARIABLE = IElementType("VARIABLE", BerryCrushLanguage)

    // Table
    val PIPE = IElementType("PIPE", BerryCrushLanguage)
    val TABLE_CELL = IElementType("TABLE_CELL", BerryCrushLanguage)
}

Composite Types

Built by the parser:

object BerryCrushElementTypes {
    val FEATURE = BerryCrushElementType("FEATURE")
    val SCENARIO = BerryCrushElementType("SCENARIO")
    val FRAGMENT = BerryCrushElementType("FRAGMENT")
    val STEP = BerryCrushElementType("STEP")
    val DATA_TABLE = BerryCrushElementType("DATA_TABLE")
    val INCLUDE = BerryCrushElementType("INCLUDE")
}

References

Fragment Reference

class BerryCrushFragmentReference(
    element: BerryCrushIncludeElement
) : PsiReferenceBase<BerryCrushIncludeElement>(element) {

    override fun resolve(): PsiElement? {
        val fragmentName = element.fragmentName
        return findFragmentByName(fragmentName)
    }

    override fun getVariants(): Array<Any> {
        return getAllFragmentNames().map { name ->
            LookupElementBuilder.create(name)
                .withIcon(BerryCrushIcons.FRAGMENT)
        }.toTypedArray()
    }

    private fun findFragmentByName(name: String): BerryCrushFragmentElement? {
        return StubIndex.getElements(
            FragmentNameIndex.KEY,
            name,
            element.project,
            GlobalSearchScope.projectScope(element.project),
            BerryCrushFragmentElement::class.java
        ).firstOrNull()
    }
}

Operation Reference

class BerryCrushOperationReference(
    element: BerryCrushOperationRefElement
) : PsiReferenceBase<BerryCrushOperationRefElement>(element) {

    override fun resolve(): PsiElement? {
        val operationId = element.operationId
        val openApiService = element.project.service<OpenApiService>()
        return openApiService.getOperationElement(operationId)
    }
}

Stubs

Stubs enable faster indexing by storing essential data without full PSI parsing.

Fragment Stub

interface BerryCrushFragmentStub : StubElement<BerryCrushFragmentElement> {
    val name: String
}

class BerryCrushFragmentStubImpl(
    parent: StubElement<*>,
    override val name: String
) : StubBase<BerryCrushFragmentElement>(parent, BerryCrushStubTypes.FRAGMENT),
    BerryCrushFragmentStub

Stub Element Type

object BerryCrushFragmentStubType : 
    IStubElementType<BerryCrushFragmentStub, BerryCrushFragmentElement>("FRAGMENT", BerryCrushLanguage) {

    override fun createStub(
        psi: BerryCrushFragmentElement,
        parentStub: StubElement<*>
    ): BerryCrushFragmentStub {
        return BerryCrushFragmentStubImpl(parentStub, psi.name ?: "")
    }

    override fun serialize(stub: BerryCrushFragmentStub, dataStream: StubOutputStream) {
        dataStream.writeName(stub.name)
    }

    override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>): BerryCrushFragmentStub {
        val name = dataStream.readName()?.string ?: ""
        return BerryCrushFragmentStubImpl(parentStub, name)
    }

    override fun createPsi(stub: BerryCrushFragmentStub): BerryCrushFragmentElement {
        return BerryCrushFragmentElementImpl(stub, this)
    }
}

Traversal

Finding Elements

// Find first matching element
val feature = PsiTreeUtil.findChildOfType(file, BerryCrushFeatureElement::class.java)

// Find all matching elements
val scenarios = PsiTreeUtil.findChildrenOfType(file, BerryCrushScenarioElement::class.java)

// Find parent
val scenario = PsiTreeUtil.getParentOfType(step, BerryCrushScenarioElement::class.java)

// Find sibling
val nextStep = PsiTreeUtil.getNextSiblingOfType(step, BerryCrushStepElement::class.java)

Visitor Pattern

class BerryCrushVisitor : PsiElementVisitor() {
    fun visitFragment(element: BerryCrushFragmentElement) {
        visitElement(element)
    }

    fun visitScenario(element: BerryCrushScenarioElement) {
        visitElement(element)
    }

    fun visitStep(element: BerryCrushStepElement) {
        visitElement(element)
    }
}

// Usage
file.accept(object : BerryCrushVisitor() {
    override fun visitFragment(element: BerryCrushFragmentElement) {
        // Process fragment
    }
})

Element Factory

Create new PSI elements programmatically:

object BerryCrushElementFactory {
    fun createIdentifier(project: Project, name: String): PsiElement {
        val text = "Fragment: $name\n  Given step"
        val file = createFile(project, text)
        return file.firstChild.findChildByType(BerryCrushTypes.TEXT)!!
    }

    fun createFragment(project: Project, name: String): BerryCrushFragmentElement {
        val text = "Fragment: $name\n  Given placeholder step"
        val file = createFile(project, text)
        return PsiTreeUtil.findChildOfType(file, BerryCrushFragmentElement::class.java)!!
    }

    private fun createFile(project: Project, text: String): BerryCrushFile {
        return PsiFileFactory.getInstance(project)
            .createFileFromText("dummy.fragment", BerryCrushFileType, text) as BerryCrushFile
    }
}

Best Practices

Thread Safety

Always access PSI in read/write actions:

// Read
ApplicationManager.getApplication().runReadAction {
    val name = element.name
}

// Write
WriteCommandAction.runWriteCommandAction(project) {
    element.setName(newName)
}

Smart Pointers

Don't hold PSI references across operations:

// Bad - element may become invalid
val element = findElement()
// ... later ...
element.name  // May throw

// Good - use smart pointer
val pointer = SmartPointerManager.createPointer(element)
// ... later ...
pointer.element?.name  // Safe

Avoid Redundant Parsing

Use stubs when possible:

// Slower - parses full file
val fragments = PsiManager.getInstance(project)
    .findFile(virtualFile)
    ?.children
    ?.filterIsInstance<BerryCrushFragmentElement>()

// Faster - uses stub index
val fragments = StubIndex.getElements(
    FragmentNameIndex.KEY,
    name,
    project,
    scope,
    BerryCrushFragmentElement::class.java
)