Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Implementing JSON Formatter Plugins for JetBrains IDEs

JetBrains IDEs like IntelliJ IDEA, PyCharm, WebStorm, and others are renowned for their powerful code formatting capabilities. While they come with excellent built-in formatters for many languages, you might encounter scenarios where you need a custom formatter for a specific JSON dialect, a configuration file using JSON-like syntax, or simply want to tailor the default formatting behavior more precisely than the standard settings allow. This is where plugin development comes in.

Implementing a formatter as a JetBrains plugin involves diving into the IDE's internal APIs, particularly those related to parsing, syntax trees, and code style. This article provides a conceptual overview and guides you through the key components required to build a JSON formatter plugin.

Understanding the JetBrains Platform

JetBrains IDEs are built on the IntelliJ Platform. Plugins extend the functionality of this platform. For language-specific features like parsing, highlighting, code completion, and formatting, you'll typically develop a "Language Plugin". A formatter is a specific component within a language plugin.

The core concept behind most IDE features, including formatting, is the Abstract Syntax Tree (AST) or more specifically, the Program Structure Interface (PSI). The IDE first parses your code into a PSI tree, which is a hierarchical representation of the code's structure. The formatter then traverses this tree and applies formatting rules.

Key Components for Formatting

To implement a formatter, you'll need to define several components and link them together via your plugin's configuration file (`plugin.xml`). The main components involved in formatting are:

  • Language Definition: You need to define a specific com.intellij.lang.Language instance for JSON (or your custom JSON-like language). While a standard JSON language is built-in, for a custom formatter, you might need to override aspects or define a new language ID if it's a variation.
  • Parser Definition: A com.intellij.lang.ParserDefinition tells the IDE how to parse your language's text into a PSI tree. This is crucial because the formatter operates on this tree structure.
  • Formatter Provider: A com.intellij.psi.codeStyle.LanguageInjector is used to inject a formatter into specific contexts, but for a primary language formatter, you mostly rely on standard extensions. The core formatting logic is provided via a com.intellij.psi.codeStyle.FormattingModelBuilder.
  • Formatting Model Builder: The FormattingModelBuilder creates a com.intellij.psi.codeStyle.FormattingModel for a given PSI element. This model contains the structure (using "Blocks") and rules for formatting.
  • Block Implementation: This is the heart of the formatter. You'll create classes implementing com.intellij.formatting.Block (often by extending com.intellij.formatting.AbstractBlock or similar base classes provided by the platform). Each Block corresponds to a node or a range of nodes in the PSI tree and is responsible for:
    • Providing child Blocks.
    • Defining indentation rules relative to its parent.
    • Defining spacing rules before and after itself and its children.
    • Defining wrapping rules (e.g., whether an array should wrap items).
    • Handling alignment.
  • Code Style Settings: To make your formatter configurable via the IDE's settings dialog, you'll implement classes extending com.intellij.psi.codeStyle.CodeStyleSettings and integrate a settings panel using Swing or Kotlin UI DSL.

Conceptual Implementation Steps

Here's a simplified outline of the process you'd follow in Java or Kotlin within the IntelliJ Platform SDK:

1. Set up the Development Environment

You'll need the IntelliJ Platform SDK, typically managed via Gradle. Create a new plugin project using the SDK.

2. Define Your Language and Parser

Create classes for your language (e.g., JsonishLanguage) and its parser definition (e.g., JsonishParserDefinition). The parser definition links your lexer and parser implementations to the language.

Example plugin.xml snippet (conceptual):

<extensions defaultExtensionNs="com.intellij">
  <language implementationClass="com.example.JsonishLanguage" id="Jsonish" />
  <lang.parserDefinition implementationClass="com.example.JsonishParserDefinition" lang="Jsonish" />
  <!-- Other language features like Lexer, Commenter, etc. -->
</extensions>

3. Implement the Formatting Model Builder

Create a class (e.g., JsonishFormattingModelBuilder) that implements FormattingModelBuilder. Its main method, createModel, will take a PSI element and return a FormattingModel. This model is built around your custom Block implementation.

Example plugin.xml snippet (conceptual):

<extensions defaultExtensionNs="com.intellij">
  <!-- ... language and parser definitions ... -->
  <lang.formatter implementationClass="com.example.JsonishFormattingModelBuilder" lang="Jsonish" />
</extensions>

4. Develop Your Block Implementation

This is the most complex part. You'll likely create a class like JsonishBlock (extending AbstractBlock or similar).

Inside your JsonishBlock:

  • The buildChildren() method recursively creates child Block objects for the children of the current PSI element.
  • The getIndent() method returns the indentation for the current block based on its type (e.g., increase indent inside objects and arrays).
  • The getSpacing() method defines spaces between child elements (e.g., space after colon, space after comma). This uses the com.intellij.formatting.SpacingBuilder.
  • The getWrap() method defines wrapping rules (e.g., wrap array elements if they exceed line length).
  • The getAlignment() method defines alignment rules (e.g., align colons in an object).

You'll need to identify the specific PSI element types produced by your parser (e.g., object, array, property, key, value, comma, colon) and write logic in your JsonishBlock to handle the formatting rules for each type.

5. Add Code Style Settings (Optional but Recommended)

Implement classes to manage settings and create a UI panel. This allows users to customize indentation size, spacing options, etc., for your formatter. This involves working with com.intellij.psi.codeStyle.CodeStyleSettingsProvider and com.intellij.psi.codeStyle.CustomCodeStyleSettings.

Example plugin.xml snippet (conceptual):

<extensions defaultExtensionNs="com.intellij">
  <!-- ... other definitions ... -->
  <codeStyleSettingsProvider implementation="com.example.JsonishCodeStyleSettingsProvider" />
</extensions>

Formatting Logic Details

Implementing the Block logic is where most of your effort will go.

Indentation

Use com.intellij.formatting.Indent.getIndent() factory methods. Common types include:

  • Indent.getNoneIndent(): No indentation (for elements at the top level or within containers that don't indent their children).
  • Indent.getNormalIndent(): Standard indentation level (for children of objects/arrays).
  • Indent.getSmartIndent(type): Context-aware indentation.

You'll typically define that children of object ({...}) and array ([{...}]) PSI elements get NormalIndent relative to their parent block.

Conceptual Indentation Logic (within a Block class):

// In JsonishBlock.getIndent()
if (psiElement.getParent() != null &&
    (psiElement.getParent().getNode().getElementType() == JsonishElementTypes.OBJECT ||
     psiElement.getParent().getNode().getElementType() == JsonishElementTypes.ARRAY)) {
  return Indent.getNormalIndent();
}
return Indent.getNoneIndent();

Spacing

Use com.intellij.formatting.SpacingBuilder. You define rules like:

  • Space before/after tokens (e.g., require one space after a colon).
  • Spaces around operators (not applicable to standard JSON, but relevant for JSON-like syntaxes).
  • Blank lines (e.g., minimum blank lines between top-level elements).

Conceptual Spacing Logic (using SpacingBuilder):

// In JsonishFormattingModelBuilder, initialize SpacingBuilder
private static SpacingBuilder createSpaceBuilder(CodeStyleSettings settings) {
  return new SpacingBuilder(settings, JsonishLanguage.INSTANCE)
    .after(JsonishTokenTypes.COLON).singleSpace() // Space after colon
    .before(JsonishTokenTypes.COLON).none()      // No space before colon
    .after(JsonishTokenTypes.COMMA).singleSpace() // Space after comma
    .around(JsonishTokenTypes.EQ).singleSpace()   // Example for '=' if syntax uses it
    .withinPair(JsonishTokenTypes.LBRACE, JsonishTokenTypes.RBRACE).spaceIf(settings.SPACE_WITHIN_BRACES, true) // Space inside {}
    .withinPair(JsonishTokenTypes.LBRACKET, JsonishTokenTypes.RBRACKET).spaceIf(settings.SPACE_WITHIN_BRACKETS, true); // Space inside []
}

// In JsonishBlock.getSpacing()
return spacingBuilder.getSpacing(getParent(), psiElement, child.getNode());

Wrapping and Alignment

Use com.intellij.formatting.Wrap.createWrap() and com.intellij.formatting.Alignment.createAlignment(). These are used within your Block's buildChildren() method to associate wrapping or alignment rules with specific child blocks.

  • Wrapping: You can enforce wrapping (Wrap.createWrap(WrapType.ALWAYS, true)), wrap if needed (WrapType.CHOP_DOWN_IF_NECCESSARY), etc. Useful for putting object properties or array elements on new lines.
  • Alignment: Useful for aligning elements like the colons in object properties or values in an array.

Conceptual Wrap/Alignment (within buildChildren):

// Inside JsonishBlock.buildChildren() loop, when adding child blocks:
// Example: Wrap and align property values in an object
if (childPsi.getNode().getElementType() == JsonishElementTypes.PROPERTY_VALUE) {
  Block childBlock = new JsonishBlock(childPsi, getAlignment(), getIndent(), getWrap(), spacingBuilder, settings);
  blocks.add(childBlock);
} else {
  // ... handle other types ...
}

Putting It Together (Conceptual)

Your FormattingModelBuilder will typically:

  1. Get the root PSI element for the file/code fragment.
  2. Create a root JsonishBlock for this element.
  3. Initialize a SpacingBuilder with rules based on the current code style settings.
  4. Pass the SpacingBuilder and settings down through the recursive creation of child blocks in the buildChildren() method of your JsonishBlock.
  5. The getSpacing() method in each block uses the SpacingBuilder to determine spacing between its own children.
  6. The getIndent(), getWrap(), and getAlignment() methods define rules applied *to* the current block relative to its parent, or applied *to* its children.

The IDE's formatting engine then uses this model of blocks and rules to calculate the desired layout and apply changes to the document.

Challenges and Tips

  • PSI Tree Understanding: The biggest hurdle is understanding the structure of the PSI tree generated by your parser (or the standard JSON parser). Use the "PsiViewer" plugin in the IDE Dev instance to inspect the tree for example JSON code.
  • SpacingBuilder Complexity: Configuring the SpacingBuilder for all token types and contexts can be tricky. Start simple and add rules incrementally.
  • Debugging: Debugging formatter code can be challenging. Use logging and the IDE's debugging tools extensively while running a Dev instance of the IDE with your plugin.
  • Performance: Formatting is a frequent operation. Ensure your block creation and rule application logic is efficient to avoid performance bottlenecks. Avoid excessive PSI traversals within formatting methods.
  • Testing: Write tests for your formatter. The platform provides testing utilities for formatting.

Conclusion

Building a custom formatter plugin for JetBrains IDEs, even for a seemingly simple format like JSON, is a non-trivial task that requires understanding the IntelliJ Platform's architecture, particularly its PSI and formatting APIs. However, it offers immense power to create highly tailored code style experiences. By defining your language elements, building a robust PSI tree, and implementing the Block structure with precise rules for indentation, spacing, wrapping, and alignment, you can integrate a professional-grade formatter directly into the developer's workflow within their preferred IDE. While this article provides a high-level overview, the actual implementation details reside within the IntelliJ Platform SDK documentation and require coding in Java or Kotlin.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool