Need help with your JSON?

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

Creating Atom Editor Packages for JSON Formatting

The Atom text editor, built on Electron, offers a highly customizable environment for developers. One of its most powerful features is its package system, allowing users to extend its functionality significantly. Creating a package to format JSON is a common and practical use case, providing a quick way to pretty-print unformatted or minified JSON directly within the editor. This guide will walk you through the process, from setting up your package structure to implementing the core formatting logic.

Understanding Atom Packages

Atom packages are essentially Node.js modules with a specific structure that Atom understands. They consist of a main entry file and a package.json file, similar to standard npm packages, but with Atom-specific configuration.

Key components of an Atom package:

  • package.json: Contains metadata about the package (name, version, description, etc.) and crucially defines package activation/deactivation methods and commands.
  • Main file (e.g., index.js or main.js): Exports functions like activate() and deactivate(), which are called when the package is enabled or disabled. This file contains the package's core logic.
  • Optional directories: lib/ for source code, menus/ for menu definitions, styles/ for CSS, etc.

Setting up Your Package

The easiest way to start is by using Atom's built-in package generator.

  • Go to Edit > Developer > Generate New Package...
  • Choose a directory for your package (e.g., json-formatter-package).
  • Enter the package name (e.g., json-formatter-package).

This will create a basic directory structure with a package.json, a main JavaScript file, and some example files.

The package.json File

Your generated package.json will have some basic fields. For a JSON formatter, the important parts are the activationCommands and potentially config for user-configurable settings (like indentation).

Example package.json:

{
  "name": "json-formatter-package",
  "main": "./lib/json-formatter-package", // Or whatever your main file is named
  "version": "1.0.0",
  "description": "Formats JSON within Atom Editor",
  "keywords": [],
  "activationCommands": {
    "atom-text-editor": "json-formatter-package:format"
  },
  "repository": "https://github.com/your-username/json-formatter-package",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {},
  "config": {
    "indentation": {
      "title": "Indentation Spaces",
      "description": "Number of spaces to use for indentation",
      "type": "integer",
      "default": 2,
      "minimum": 0
    }
  }
}
  • main: Points to the package's entry file.
  • activationCommands: This tells Atom to activate your package when the specified command is triggered on the specified element (atom-text-editor means any active text editor). The format is 'target-element': 'command-name'.
  • config: Defines package settings that users can modify via Atom's Settings view.

The Main Package File (e.g., lib/json-formatter-package.js)

This file will contain the activate and deactivate functions, and the logic for your command.

Example Main File Structure:

export default {
  // Called when the package is activated
  activate() {
    // Register the command
    this.commandDisposable = atom.commands.add('atom-text-editor', {
      'json-formatter-package:format': () => this.formatJson()
    });

    // Optional: Watch config changes if needed immediately (less common for simple formatters)
    // this.configDisposable = atom.config.observe('json-formatter-package.indentation', (value) => {
    //   this.indentation = value;
    // });
  },

  // Called when the package is deactivated
  deactivate() {
    this.commandDisposable.dispose();
    // this.configDisposable?.dispose(); // Dispose of config observer if used
  },

  // Placeholder for the core formatting logic
  formatJson() {
    // Implementation goes here
    console.log('json-formatter-package:format command triggered!');
    atom.notifications.addInfo('JSON Format command triggered (implementation missing).');
  }
};
  • activate(): Where you set up event listeners, register commands, etc. We register our json-formatter-package:format command using atom.commands.add. The handler calls our formatJson method. A Disposable object is returned and stored, which is needed for cleanup.
  • deactivate(): Where you clean up anything created in activate() to prevent memory leaks. Disposing the command disposable is crucial.
  • formatJson(): This is the function that will execute when the command is triggered.

Implementing the JSON Formatting Logic

Now, let's fill in the formatJson method. The steps are: get the editor, get the text, parse it, format it, and replace the text in the editor.

Full formatJson Implementation:

formatJson() {
  const editor = atom.workspace.getActiveTextEditor();
  if (!editor) {
    atom.notifications.addWarning('JSON Formatter: No active text editor.');
    return;
  }

  // Get the selected text, or the entire buffer text if nothing is selected
  const selection = editor.getSelectedText();
  const fullText = editor.getBuffer().getText();
  const textToFormat = selection || fullText;

  if (!textToFormat) {
     atom.notifications.addInfo('JSON Formatter: No text to format.');
     return;
  }

  let parsedJson;
  try {
    parsedJson = JSON.parse(textToFormat);
  } catch (error) {
    atom.notifications.addError(`JSON Formatter: Invalid JSON input.`, {
      detail: error.message,
      dismissable: true
    });
    return;
  }

  // Get indentation setting from package config
  // Note: atom.config.get returns the config value.
  // If you used atom.config.observe as shown commented out above,
  // you might store it in this.indentation
  const indentationSpaces = atom.config.get('json-formatter-package.indentation') || 2;

  let formattedJson;
  try {
    // JSON.stringify with null replacer and indentationSpaces for pretty printing
    formattedJson = JSON.stringify(parsedJson, null, indentationSpaces);
  } catch (error) {
     // Should theoretically not happen if parsing succeeded, but good practice
     atom.notifications.addError(`JSON Formatter: Error during stringification.`, {
       detail: error.message,
       dismissable: true
     });
     return;
  }


  // Replace the text in the editor
  if (selection) {
     // If text was selected, replace only the selection
     editor.setText(formattedJson); // This replaces the *entire* buffer, NOT just the selection. Correcting this...
     // To replace selection: editor.insertText(formattedJson);
     // Let's re-evaluate: should it replace selection or entire file?
     // A common pattern is to replace the selection if one exists, otherwise the whole file.
     // Let's implement replace selection.
     editor.insertText(formattedJson); // This inserts where the cursor is, replacing selection if one exists.
  } else {
    // If no text was selected, replace the entire buffer
    editor.getBuffer().setText(formattedJson);
  }

   atom.notifications.addSuccess('JSON Formatter: Document formatted successfully.');
}

Explanation of the Logic:

  • atom.workspace.getActiveTextEditor(): Retrieves the currently focused text editor instance. Returns undefined if no editor is active.
  • editor.getSelectedText(): Gets the text of the current selection. Returns an empty string if nothing is selected.
  • editor.getBuffer().getText(): Gets the entire content of the text buffer.
  • textToFormat = selection || fullText;: This is a common pattern. If there's a selection, format that; otherwise, format the whole file.
  • JSON.parse(textToFormat): Attempts to parse the input string into a JavaScript object/array. This is where invalid JSON will throw an error.
  • atom.config.get(...): Reads the value of the configuration setting defined in package.json.
  • JSON.stringify(parsedJson, null, indentationSpaces): Converts the JavaScript object/array back into a JSON string.
    • The first argument is the value to stringify.
    • The second argument (null) is the replacer function; null means include all properties.
    • The third argument (indentationSpaces) is the space count for indentation, controlling the pretty-printing. Using a number formats the output nicely.
  • editor.insertText(formattedJson): If there was a selection, this method replaces the selection with the new text and places the cursor at the end of the inserted text.
  • editor.getBuffer().setText(formattedJson): If no selection, this replaces the entire content of the editor's buffer with the formatted text.
  • atom.notifications.addError/addWarning/addSuccess: Used to provide feedback to the user via Atom's notification system.

Handling Configuration

As shown in the package.json example, you can define configuration options. Atom automatically creates a UI for these settings in the package manager.

Accessing config values in your code is done via atom.config.get('your-package-name.setting-name'). We used this to get the indentation level.

If you needed to react immediately to config changes (e.g., if the indentation could change how something is displayed live, which isn't the case for a formatter that only runs on command), you would use atom.config.observe in your activate function and dispose of the observer in deactivate.

Making it JSON Specific (Optional but Recommended)

Currently, our package would format any text in any file type if the command is triggered. You might want it to work only in JSON files. You can achieve this by:

  • Checking the file's grammar: Inside formatJson, check editor.getGrammar().scopeName. JSON files typically have a scope name like source.json. Add a check at the beginning:
    // Inside formatJson()
    const editor = atom.workspace.getActiveTextEditor();
    // ... (null editor check) ...
    
    // Check if the current grammar is JSON
    if (editor.getGrammar().scopeName !== 'source.json') {
      atom.notifications.addWarning('JSON Formatter: This command is intended for JSON files.');
      return;
    }
  • Contextualizing the command: You can change activationCommands to be more specific, but checking the grammar inside the command handler is often more flexible.

Putting It All Together (Simplified Main File)

lib/json-formatter-package.js:

export default {
  commandDisposable: null,

  activate() {
    this.commandDisposable = atom.commands.add('atom-text-editor', {
      'json-formatter-package:format': () => this.formatJson()
    });
  },

  deactivate() {
    if (this.commandDisposable) {
      this.commandDisposable.dispose();
      this.commandDisposable = null;
    }
  },

  formatJson() {
    const editor = atom.workspace.getActiveTextEditor();
    if (!editor) {
      atom.notifications.addWarning('JSON Formatter: No active text editor.');
      return;
    }

    // Optional: Check if it's a JSON file
    if (editor.getGrammar().scopeName !== 'source.json') {
       atom.notifications.addWarning('JSON Formatter: This command is intended for JSON files.');
       return;
    }


    const selection = editor.getSelectedText();
    const textToFormat = selection || editor.getBuffer().getText();

    if (!textToFormat) {
       atom.notifications.addInfo('JSON Formatter: No text to format.');
       return;
    }

    let parsedJson;
    try {
      parsedJson = JSON.parse(textToFormat);
    } catch (error) {
      atom.notifications.addError(`JSON Formatter: Invalid JSON input.`, {
        detail: error.message,
        dismissable: true
      });
      return;
    }

    // Get indentation setting, defaulting to 2 spaces
    const indentationSpaces = atom.config.get('json-formatter-package.indentation') || 2;

    let formattedJson;
    try {
      formattedJson = JSON.stringify(parsedJson, null, indentationSpaces);
    } catch (error) {
       atom.notifications.addError(`JSON Formatter: Error during stringification.`, {
         detail: error.message,
         dismissable: true
       });
       return;
    }

    // Replace text
    if (selection) {
       editor.insertText(formattedJson); // Replace selection
    } else {
      editor.getBuffer().setText(formattedJson); // Replace entire buffer
    }

     atom.notifications.addSuccess('JSON Formatter: Document formatted successfully.');
  }
};

Testing Your Package

Atom has a development mode that makes testing packages easy:

  • Go to Edit > Developer > Open in Dev Mode...
  • Select the folder where you created your package.

This opens a new Atom window with your package loaded. Open a .json file (or any file), type or paste some JSON, select it (or not), and then trigger your command.

To find the command, you can open the Command Palette (Cmd/Ctrl+Shift+P) and search for your package name or command (e.g., "JSON Format"). You can also bind it to a key combination in your keymap file (Edit > Keymap...).

Example keymap.cson entry:

'atom-text-editor[data-grammar="source json"]':
  'cmd-alt-f': 'json-formatter-package:format'

This example binds the command to Cmd+Alt+F (or Ctrl+Alt+F on Windows/Linux) specifically when editing a file with the source.json grammar.

Further Enhancements

Now that you have a basic formatter, consider these improvements:

  • Add more configuration options: Allow users to choose tabs vs. spaces, or specify a custom indentation string.
  • Handle different JSON flavors: Some APIs return JSONP or other slightly non-standard formats. You might need a more robust parsing library.
  • Provide a menu item: Define a menu entry in a menus/your-package-name.json file to make the command accessible from Atom's menu bar.
  • Context menu integration: Add the command to the right-click context menu.
  • Error presentation: Instead of just notifications, highlight the location of the JSON error in the editor.

Conclusion

Creating an Atom package for JSON formatting is a great way to learn about Atom's API and package development. The core logic is surprisingly simple thanks to JavaScript's built-in JSON.parse and JSON.stringify methods. By leveraging Atom's commands and editor APIs, you can build a useful tool that integrates seamlessly into your workflow. This foundation can be extended to build more complex text processing or editor enhancing packages.

Need help with your JSON?

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