Skip to content

Architecture

This document describes the internal architecture of the BerryCrush IntelliJ plugin.

Overview

The plugin follows IntelliJ Platform architecture patterns:

┌─────────────────────────────────────────────────────────────┐
│                     IntelliJ Platform                        │
├─────────────────────────────────────────────────────────────┤
│  Extension Points (Language, Editor, Navigation, etc.)      │
├─────────────────────────────────────────────────────────────┤
│                    BerryCrush Plugin                         │
│  ┌──────────────┬──────────────┬──────────────────────────┐ │
│  │   Language   │   Services   │     Functionality        │ │
│  │   Support    │              │                          │ │
│  │  ├─ Parser   │  ├─ OpenAPI  │  ├─ Navigation           │ │
│  │  ├─ Lexer    │  │  Provider │  ├─ Completion           │ │
│  │  └─ PSI      │  ├─ Fragment │  ├─ Refactoring          │ │
│  │              │  │  Index    │  ├─ Inspections          │ │
│  │              │  └─ Step     │  └─ Quick Fixes          │ │
│  │              │     Registry │                          │ │
│  └──────────────┴──────────────┴──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Package Structure

org.berrycrush.intellij/
├── language/           # Language definition
│   ├── BerryCrushLanguage.kt
│   ├── BerryCrushFileType.kt
│   └── BerryCrushIcons.kt
├── parser/             # Parsing
│   ├── BerryCrushLexer.kt
│   ├── BerryCrushParser.kt
│   └── BerryCrushParserDefinition.kt
├── psi/                # PSI elements
│   ├── BerryCrushFile.kt
│   ├── BerryCrushElements.kt
│   └── BerryCrushElementTypes.kt
├── services/           # Application/project services
│   ├── OpenApiService.kt
│   ├── FragmentIndexService.kt
│   └── StepRegistry.kt
├── navigation/         # Navigation features
│   ├── BerryCrushFindUsagesProvider.kt
│   └── BerryCrushTargetElementEvaluator.kt
├── completion/         # Code completion
│   └── BerryCrushCompletionContributor.kt
├── refactoring/        # Refactoring support
│   ├── safedelete/
│   │   └── BerryCrushSafeDeleteProcessor.kt
│   └── rename/
│       └── BerryCrushRenameProcessor.kt
├── inspections/        # Code inspections
│   ├── MissingFragmentInspection.kt
│   └── UnknownOperationInspection.kt
├── quickfix/           # Quick fixes
│   └── CreateFragmentQuickFix.kt
└── highlighting/       # Syntax highlighting
    └── BerryCrushSyntaxHighlighter.kt

Core Components

Language Registration

BerryCrushLanguage.kt - Language singleton:

object BerryCrushLanguage : Language("BerryCrush") {
    override fun getDisplayName() = "BerryCrush"
}

BerryCrushFileType.kt - File type for .scenario and .fragment:

object BerryCrushFileType : LanguageFileType(BerryCrushLanguage) {
    override fun getName() = "BerryCrush"
    override fun getDefaultExtension() = "scenario"
}

Parser

The parser converts text into PSI tree:

Text → Lexer → Tokens → Parser → PSI Tree

Lexer breaks text into tokens:

"given I call ^login" → [KEYWORD:"given", TEXT:"I call", OP_REF:"^login"]

Parser builds tree structure from tokens using grammar rules.

PSI (Program Structure Interface)

PSI elements represent code structure:

BerryCrushFile
├── ScenarioElement
│   ├── KeywordElement ("scenario:")
│   ├── TextElement ("Create user")
│   └── StepElement
│       ├── KeywordElement ("when")
│       ├── TextElement ("I call")
│       └── OperationRefElement ("^createUser")
└── FragmentElement
    ├── KeywordElement ("fragment:")
    └── TextElement ("login-steps")

Services

OpenAPI Service

Purpose: Parse and query OpenAPI specifications.

Lifecycle: Project-level service, lazy initialization.

@Service(Service.Level.PROJECT)
class OpenApiService(private val project: Project) {
    fun getOperation(operationId: String): Operation?
    fun getAllOperations(): List<Operation>
    fun getOperationsFromFile(file: VirtualFile): List<Operation>
}

Fragment Index Service

Purpose: Index fragment definitions and includes for fast lookup.

Uses: IntelliJ's stub indexing infrastructure.

object IncludeUsageIndex : StringStubIndexExtension<BerryCrushIncludeElement>() {
    fun getFragmentUsages(name: String, project: Project): Collection<PsiElement>
}

Step Registry

Purpose: Track custom @Step and @Assertion annotated methods.

@Service(Service.Level.PROJECT)
class StepRegistry(private val project: Project) {
    fun findStep(text: String): PsiMethod?
    fun findAssertion(text: String): PsiMethod?
}

Extension Points

Used Extension Points

Extension Point Implementation
lang.parserDefinition BerryCrushParserDefinition
lang.foldingBuilder BerryCrushFoldingBuilder
completion.contributor BerryCrushCompletionContributor
gotoDeclarationHandler BerryCrushGotoHandler
findUsagesProvider BerryCrushFindUsagesProvider
refactoring.safeDeleteProcessor BerryCrushSafeDeleteProcessor
annotator BerryCrushAnnotator
localInspection Various inspections

Runtime Highlighting Model

Runtime editor highlighting for BerryCrush files is annotator-driven.

  • BerryCrushAnnotator applies token-level text attributes at runtime.
  • Context-aware highlights (for example include-parameter keys before :) are also produced by annotator logic.
  • BerryCrushSyntaxHighlighter remains available for color settings preview and token-to-color mapping reuse.

Current rule for narrative lines: - On feature: / scenario: / outline: lines, only the keyword prefix is highlighted. - On given / when / then / and / but lines, only the step keyword prefix is highlighted. - Text after these prefixes is treated as narrative description and must remain uncolored by BerryCrush syntax keys. - Include parameter key highlighting remains active for include parameter entries.

Provided Extension Points

The plugin provides extension points for customization:

<extensionPoint name="stepProvider" interface="...StepProvider"/>
<extensionPoint name="assertionProvider" interface="...AssertionProvider"/>

Data Flow

Formatting Flow

Formatting is a two-stage pipeline:

Reformat Code
    -> BerryCrushFormattingModelBuilder / BerryCrushBlock (token spacing + indent hints)
    -> BerryCrushPostFormatProcessor (structural indentation + table alignment)

BerryCrushPostFormatProcessor is the source of truth for final structural indentation because BerryCrush PSI is largely flat for many constructs.

Core structural rules: - Top-level blocks (feature, fragment, standalone scenario, standalone outline) are root-aligned. - Steps (given/when/then/and/but) are indented one level under scenario/outline/background. - Directives (call, assert, extract, include, webhook) are one level under steps. - Directive payload keys (id, body, port, hook, etc.) are one level under directives. - Nested map payloads increase indentation by one level per map depth. - examples: is outline-scoped and table rows are one level under examples:.

Regression coverage for these rules is in: - ../../src/test/kotlin/org/berrycrush/intellij/formatting/BerryCrushFormattingTest.kt

Completion Flow

1. User types "^"
2. Platform triggers CompletionContributor
3. BerryCrushCompletionContributor.fillCompletionVariants()
4. Query OpenApiService for operations
5. Create LookupElements for each operation
6. Return to platform for display
1. User Ctrl+clicks on "^operationId"
2. Platform triggers GotoDeclarationHandler
3. BerryCrushGotoHandler.getGotoDeclarationTargets()
4. Query OpenApiService for operation location
5. Return PsiElement at definition
6. Platform navigates to element

Inspection Flow

1. File is edited
2. Platform schedules inspection pass
3. Each LocalInspectionTool.checkFile() called
4. MissingFragmentInspection checks include directives
5. Problems registered with ProblemDescriptors
6. Platform displays highlights

Threading Model

Read/Write Actions

All PSI access must be wrapped appropriately:

// Reading PSI
ApplicationManager.getApplication().runReadAction {
    // Access PSI elements
}

// Writing PSI
WriteCommandAction.runWriteCommandAction(project) {
    // Modify PSI elements
}

Background Processing

Long operations use background tasks:

ProgressManager.getInstance().run(
    object : Task.Backgroundable(project, "Processing...") {
        override fun run(indicator: ProgressIndicator) {
            // Long-running work
        }
    }
)

Indexing

Stub Indexes

Stubs provide persistent, serialized PSI subset for fast queries:

class BerryCrushFragmentStubElementType : IStubElementType<...>(...) {
    override fun createStub(psi: BerryCrushFragmentElement, parent: StubElement<*>)
    override fun serialize(stub: BerryCrushFragmentStub, dataStream: StubOutputStream)
    override fun deserialize(dataStream: StubInputStream, parent: StubElement<*>)
}

File-Based Indexes

For cross-file data:

class IncludeUsageIndex : ScalarIndexExtension<String>() {
    override fun getIndexer(): DataIndexer<String, Void, FileContent>
    override fun getKeyDescriptor(): KeyDescriptor<String>
}

Best Practices

PSI Immutability

Never modify PSI directly. Use WriteCommandAction:

WriteCommandAction.runWriteCommandAction(project, "Rename") {
    element.setName(newName)
}

Service Lifecycle

Use proper service levels: - Service.Level.APP - Shared across all projects - Service.Level.PROJECT - Per-project instance

Memory Management

Avoid holding PSI references. Use SmartPsiElementPointer:

val pointer = SmartPointerManager.createPointer(element)
// Later...
val element = pointer.element // May be null if invalidated

Testing

Use IntelliJ test framework:

class MyTest : BasePlatformTestCase() {
    fun testSomething() {
        myFixture.configureByText("test.scenario", "...")
        // Test logic
    }
}

For formatter changes, always run targeted regression tests first:

./gradlew test --tests "org.berrycrush.intellij.formatting.BerryCrushFormattingTest"