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 acom.intellij.psi.codeStyle.FormattingModelBuilder
. - Formatting Model Builder: The
FormattingModelBuilder
creates acom.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 extendingcom.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 childBlock
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 thecom.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:
- Get the root PSI element for the file/code fragment.
- Create a root
JsonishBlock
for this element. - Initialize a
SpacingBuilder
with rules based on the current code style settings. - Pass the
SpacingBuilder
and settings down through the recursive creation of child blocks in thebuildChildren()
method of yourJsonishBlock
. - The
getSpacing()
method in each block uses theSpacingBuilder
to determine spacing between its own children. - The
getIndent()
,getWrap()
, andgetAlignment()
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