Chapter 3 : Hacking Atom

About 1 min

STOP

This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.

Hacking Atom

Now it's time to come to the "Hackable" part of the Hackable Editor. As we've seen throughout the second section, a huge part of Atom is made up of bundled packages. If you wish to add some functionality to Atom, you have access to the same APIs and tools that the core features of Atom has. From the tree-viewopen in new window to the command-paletteopen in new window to find-and-replaceopen in new window functionality, even the most core features of Atom are implemented as packages.

In this chapter, we're going to learn how to extend the functionality of Atom through writing packages. This will be everything from new user interfaces to new language grammars to new themes. We'll learn this by writing a series of increasingly complex packages together, introducing you to new APIs and tools and techniques as we need them.

If you're looking for an example using a specific API or feature, you can skip to the end of the chapter where we've indexed all the examples that way.

Tools of the Trade

To begin, there are a few things we'll assume you know, at least to some degree. Since all of Atom is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.

While much of Atom has been converted to JavaScript, a lot of older code has been left implemented in CoffeeScript because changing it would have been too risky. Additionally, Atom's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:

MyPackageView = require './my-package-view'

module.exports =
  myPackageView: null

  activate: (state) ->
    @myPackageView = new MyPackageView(state.myPackageViewState)

  deactivate: ->
    @myPackageView.destroy()

  serialize: ->
    myPackageViewState: @myPackageView.serialize()

We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Atom is also doable in JavaScript. You can brush up on CoffeeScript at coffeescript.orgopen in new window.

Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at lesscss.orgopen in new window. Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.

The Init File

When Atom finishes loading, it will evaluate init.coffee in your ~/.atom%USERPROFILE%\.atom directory, giving you a chance to run CoffeeScript code to make customizations. Code in this file has full access to Atom's APIopen in new window. If customizations become extensive, consider creating a package, which we will cover in Package: Word Count.

You can open the init.coffee file in an editor from the Atom > Init ScriptFile > Init ScriptEdit > Init Script menu. This file can also be named init.js and contain JavaScript code.

For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.coffee file to have Atom greet you with an audio beep every time it loads:

atom.beep()

Because init.coffee provides access to Atom's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the Selection APIopen in new window and Clipboard APIopen in new window to construct a Markdown link from the selected text and the clipboard contents as the URL:

atom.commands.add 'atom-text-editor', 'markdown:paste-as-link', ->
  return unless editor = atom.workspace.getActiveTextEditor()

  selection = editor.getLastSelection()
  clipboardText = atom.clipboard.read()

  selection.insertText("[#{selection.getText()}](#{clipboardText})")

Now, reload Atom and use the Command Palette to execute the new command, "Markdown: Paste As Link", by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.

Package: Word Count

Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.

Package Generator

The simplest way to start a package is to use the built-in package generator that ships with Atom. As you might expect by now, this generator is itself a separate package implemented in package-generatoropen in new window.

You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it your-name-word-count. Atom will then create that directory and fill it out with a skeleton project and link it into your ~/.atom/packages%USERPROFILE%\.atom\packages directory so it's loaded when you launch your editor next time.

Note

Note: You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on atom.ioopen in new window (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the your-name-word-count package name, you should be safe 😀

Basic generated Atom package

You can see that Atom has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.

The basic package layout is as follows:

my-package/
├─ grammars/
├─ keymaps/
├─ lib/
├─ menus/
├─ spec/
├─ snippets/
├─ styles/
├─ index.js
└─ package.json

Not every package will have (or need) all of these directories and the package generator doesn't create snippets or grammars. Let's see what some of these are so we can start messing with them.

package.json

Similar to Node modulesopen in new window, Atom packages contain a package.json file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.

In addition to some of the regular Node package.json keysopen in new window available, Atom package.json files have their own additions.

  • main: the path to the JavaScript file that's the entry point to your package. If this is missing, Atom will default to looking for an index.coffee or index.js.
  • styles: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles directory are added alphabetically.
  • keymaps: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps directory are added alphabetically.
  • menus: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus directory are added alphabetically.
  • snippets: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets directory are added alphabetically.
  • activationCommands: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate() method of your main export will be called when your package is loaded.
  • activationHooks: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks:
    • core:loaded-shell-environment for when Atom has finished loading the shell environment variables
    • scope.name:root-scope-used for when a file is opened from the specified language (e.g. source.ruby:root-scope-used)
    • language-package-name:grammar-used for when a specific language package is used (e.g., my-special-language-javascript:grammar-used)
  • workspaceOpeners: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel. By including that string in workspaceOpeners, your package will defer its activation until that URI is opened.

The package.json in the package we've just generated looks like this currently:

{
	"name": "your-name-word-count",
	"main": "./lib/your-name-word-count",
	"version": "0.0.0",
	"description": "A short description of your package",
	"activationCommands": {
		"atom-workspace": "your-name-word-count:toggle"
	},
	"repository": "https://github.com/atom/your-name-word-count",
	"license": "MIT",
	"engines": {
		"atom": ">=1.0.0 <2.0.0"
	},
	"dependencies": {}
}

If you wanted to use activationHooks, you might have:

{
	"name": "your-name-word-count",
	"main": "./lib/your-name-word-count",
	"version": "0.0.0",
	"description": "A short description of your package",
	"activationHooks": [
		"language-javascript:grammar-used",
		"language-coffee-script:grammar-used"
	],
	"repository": "https://github.com/atom/your-name-word-count",
	"license": "MIT",
	"engines": {
		"atom": ">=1.0.0 <2.0.0"
	},
	"dependencies": {}
}

One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.

WARNING

Warning: Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.

Source Code

If you want to extend Atom's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main key in your package.json file. In the package we just generated, the main package file is lib/your-name-word-count.js. The remainder of your code should be placed in the lib directory, and required from your top-level file. If the main key is not in your package.json file, it will look for index.js or index.coffee as the main entry point.

Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Atom. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.

Your package's top-level module can implement the following basic methods:

  • activate(state): This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize() method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).
  • initialize(state): (Available in Atom 1.14 and above) This optional method is similar to activate() but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize() is guaranteed to be called before everything. Use activate() if you want to be sure that the workspace is ready; use initialize() if you need to do some setup prior to your deserializers or view providers being invoked.
  • serialize(): This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate method so you can restore your view to where the user left off.
  • deactivate(): This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.
Style Sheets

Style sheets for your package should be placed in the styles directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or Lessopen in new window, but Less is recommended.

Ideally, you won't need much in the way of styling. Atom provides a standard set of components which define both the colors and UI elements for any package that fits into Atom seamlessly. You can view all of Atom's UI components by opening the styleguide: open the command palette Cmd+Shift+PCtrl+Shift+P and search for styleguide, or type Cmd+Ctrl+Shift+GCtrl+Shift+G.

If you do need special styling, try to keep only structural styles in the package style sheets. If you must specify colors and sizing, these should be taken from the active theme's ui-variables.lessopen in new window.

An optional styleSheets array in your package.json can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.

Keymaps

You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json file:

{
  "atom-workspace": {
    "ctrl-alt-o": "your-name-word-count:toggle"
  }
}

This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.

Keymaps are placed in the keymaps subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps array in your package.json can specify which keymaps to load and in what order.

Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle command is executed when pressing Alt+Ctrl+O on the atom-workspace element. Because the atom-workspace element is the parent of the entire Atom UI, this means the key combination will work anywhere in the application.

We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.

Menus are placed in the menus subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your plugin.

By default, all menus are loaded in alphabetical order. An optional menus array in your package.json can specify which menus to load and in what order.

Application Menu

It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json file that was generated for us, we'll see a section that looks like this:


"menu": [
  {
    "label": "Packages",
    "submenu": [
      {
        "label": "Word Count",
        "submenu": [
          {
            "label": "Toggle",
            "command": "your-name-word-count:toggle"
          }
        ]
      }
    ]
  }
]

This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.

Application Menu Item

When you select that menu item, it will run the your-name-word-count:toggle command, which we'll look at in a bit.

The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.

Context Menu

It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json file, we can see an auto-generated section that looks like this:

"context-menu": {
    "atom-text-editor": [
      {
        "label": "Toggle your-name-word-count",
        "command": "your-name-word-count:toggle"
      }
    ]
  }

This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Atom text editor pane.

Context Menu Entry

When you click that it will again run the your-name-word-count:toggle method in your code.

Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.

You can also add separators and submenus to your context menus. To add a submenu, provide a submenu key instead of a command. To add a separator, add an item with a single type: 'separator' key/value pair. For instance, you could do something like this:

{
  "context-menu": {
    "atom-workspace": [
      {
        "label": "Text",
        "submenu": [
          {
            "label": "Inspect Element",
            "command": "core:inspect"
          },
          {
            "type": "separator"
          },
          {
            "label": "Selector All",
            "command": "core:select-all"
          },
          {
            "type": "separator"
          },
          {
            "label": "Deleted Selected Text",
            "command": "core:delete"
          }
        ]
      }
    ]
  }
}

Developing Our Package

Currently with the generated package we have, if we run that your-name-word-count:toggle command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".

Wordcount Package is Alive Dialog

Understanding the Generated Code

Let's take a look at the code in our lib directory and see what is happening.

There are two files in our lib directory. One is the main file (lib/your-name-word-count.js), which is pointed to in the package.json file as the main file to execute for this package. This file handles the logic of the whole plugin.

The second file is a View class, lib/your-name-word-count-view.js, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.

export default class YourNameWordCountView {
	constructor(serializedState) {
		// Create root element
		this.element = document.createElement("div");
		this.element.classList.add("your-name-word-count");

		// Create message element
		const message = document.createElement("div");
		message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
		message.classList.add("message");
		this.element.appendChild(message);
	}

	// Returns an object that can be retrieved when package is activated
	serialize() {}

	// Tear down any state and detach
	destroy() {
		this.element.remove();
	}

	getElement() {
		return this.element;
	}
}

Basically the only thing happening here is that when the View class is created, it creates a simple div element and adds the your-name-word-count class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!" text to it. There is also a getElement method which returns that div. The serialize and destroy methods don't do anything and we won't have to worry about that until another example.

Notice that we're simply using the basic browser DOM methods: createElement() and appendChild().

The second file we have is the main entry point to the package. Again, because it's referenced in the package.json file. Let's take a look at that file.

import YourNameWordCountView from "./your-name-word-count-view";
import { CompositeDisposable } from "atom";

export default {
	yourNameWordCountView: null,
	modalPanel: null,
	subscriptions: null,

	activate(state) {
		this.yourNameWordCountView = new YourNameWordCountView(
			state.yourNameWordCountViewState
		);
		this.modalPanel = atom.workspace.addModalPanel({
			item: this.yourNameWordCountView.getElement(),
			visible: false,
		});

		// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
		this.subscriptions = new CompositeDisposable();

		// Register command that toggles this view
		this.subscriptions.add(
			atom.commands.add("atom-workspace", {
				"your-name-word-count:toggle": () => this.toggle(),
			})
		);
	},

	deactivate() {
		this.modalPanel.destroy();
		this.subscriptions.dispose();
		this.yourNameWordCountView.destroy();
	},

	serialize() {
		return {
			yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
		};
	},

	toggle() {
		console.log("YourNameWordCount was toggled!");
		return this.modalPanel.isVisible()
			? this.modalPanel.hide()
			: this.modalPanel.show();
	},
};

There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate. The deactivate and serialize methods are expected by Atom but optional. The toggle method is one Atom is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands section of the package.json file and in the action we have in the menu file.

The deactivate method simply destroys the various class instances we've created and the serialize method simply passes on the serialization to the View class. Nothing too exciting here.

The activate command does a number of things. For one, it is not called automatically when Atom starts up, it is first called when one of the activationCommands as defined in the package.json file are called. In this case, activate is only called the first time the toggle command is called. If nobody ever invokes the menu item or hotkey, this code is never called.

This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Atom workspace.

this.yourNameWordCountView = new YourNameWordCountView(
	state.yourNameWordCountViewState
);
this.modalPanel = atom.workspace.addModalPanel({
	item: this.yourNameWordCountView.getElement(),
	visible: false,
});

We'll ignore the state stuff for now, since it's not important for this simple plugin. The rest should be fairly straightforward.

The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the plugin so other plugins could subscribe to these events.

// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
this.subscriptions = new CompositeDisposable();

// Register command that toggles this view
this.subscriptions.add(
	atom.commands.add("atom-workspace", {
		"your-name-word-count:toggle": () => this.toggle(),
	})
);

Next we have the toggle method. This method simply toggles the visibility of the modal panel that we created in the activate method.

toggle() {
  console.log('YourNameWordCount was toggled!');
  return (
    this.modalPanel.isVisible() ?
    this.modalPanel.hide() :
    this.modalPanel.show()
  );
}

This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.

The Flow

So, let's review the actual flow in this package.

  1. Atom starts up
  2. Atom starts loading packages
  3. Atom reads your package.json
  4. Atom loads keymaps, menus, styles and the main module
  5. Atom finishes loading packages
  6. At some point, the user executes your package command your-name-word-count:toggle
  7. Atom executes the activate method in your main module which sets up the UI by creating the hidden modal view
  8. Atom executes the package command your-name-word-count:toggle which reveals the hidden modal view
  9. At some point, the user executes the your-name-word-count:toggle command again
  10. Atom executes the command which hides the modal view
  11. Eventually, Atom is shut down which can trigger any serializations that your package has defined

Tip

Tip: Keep in mind that the flow will be slightly different if you choose not to use activationCommands in your package.

Counting the Words

So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.

We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle command. If we add some code to count the words and ask the view to update itself, we'll have something like this:

toggle() {
  if (this.modalPanel.isVisible()) {
    this.modalPanel.hide();
  } else {
    const editor = atom.workspace.getActiveTextEditor();
    const words = editor.getText().split(/\s+/).length;
    this.yourNameWordCountView.setCount(words);
    this.modalPanel.show();
  }
}

Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling atom.workspace.getActiveTextEditor()open in new window.

Next we get the number of words by calling getText()open in new window on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.

Finally, we tell our view to update the word count it displays by calling the setCount() method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.

We can add this code to the end of our your-name-word-count-view.js file:

setCount(count) {
  const displayText = `There are ${count} words.`;
  this.element.children[0].textContent = displayText;
}

Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.

Note

Note: To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload command in the Command Palette). A common practice is to have two Atom windows, one for developing your package, and one for testing and reloading.

Word Count Working

Basic Debugging

You'll notice a few console.log statements in the code. One of the cool things about Atom being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.

To open up the Developer Console, press Alt+Cmd+ICtrl+Shift+I, or choose the menu option View > Developer > Toggle Developer Tools.

Developer Tools Debugging

From here you can inspect objects, run code and view console output just as though you were debugging a web site.

Testing

Your package should have tests, and if they're placed in the spec directory, they can be run by Atom.

Under the hood, Jasmine v1.3open in new window executes your tests, so you can assume that any DSL available there is also available to your package.

Running Tests

Once you've got your test suite written, you can run it by pressing Alt+Cmd+Ctrl+PAlt+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.

Spec Suite Results

You can also use the atom --test spec command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.

Summary

We've now generated, customized and tested our first plugin for Atom. Congratulations! Now let's go ahead and publish it so it's available to the world.

Package: Modifying Text

Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with ascii artopen in new window. When you run our new command with the word "cool" selected, it will be replaced with:

                                     o888
    ooooooo     ooooooo     ooooooo   888
  888     888 888     888 888     888 888
  888         888     888 888     888 888
    88ooo888    88ooo88     88ooo88  o888o

This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.

The final package can be viewed at https://github.com/atom/ascii-art.

Basic Text Insertion

To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Paletteopen in new window. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter ascii-art as the name of the package.

Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js, spec/ascii-art-view-spec.js, and styles/.

Next, open up lib/ascii-art.js and remove all view code, so it looks like this:

const { CompositeDisposable } = require("atom");

module.exports = {
	subscriptions: null,

	activate() {
		this.subscriptions = new CompositeDisposable();
		this.subscriptions.add(
			atom.commands.add("atom-workspace", {
				"ascii-art:convert": () => this.convert(),
			})
		);
	},

	deactivate() {
		this.subscriptions.dispose();
	},

	convert() {
		console.log("Convert text!");
	},
};
Create a Command

Now let's add a command. You should namespace your commands with the package name followed by a : and then the name of the command. As you can see in the code, we called our command ascii-art:convert and we will define it to call the convert() method when it's executed.

So far, that will simply log to the console. Let's start by making it insert something into the text buffer.

convert() {
  const editor = atom.workspace.getActiveTextEditor()
  if (editor) {
    editor.insertText('Hello, World!')
  }
}

As in Counting Words, we're using atom.workspace.getActiveTextEditor() to get the object that represents the active text editor. If this convert() method is called when not focused on a text editor, nothing will happen.

Next we insert a string into the current text editor with the insertText()open in new window method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.

Reload the Package

Before we can trigger ascii-art:convert, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing Alt+Cmd+Ctrl+LCtrl+Shift+F5.

Trigger the Command

Now open the Command Palette and search for the "Ascii Art: Convert" command. But it's not there! To fix this, open package.json and find the property called activationCommands. Activation commands make Atom launch faster by allowing Atom to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert in activationCommands:

"activationCommands": {
  "atom-workspace": "ascii-art:convert"
}

First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the "Ascii Art: Convert" command it will insert "Hello, World!" into the active editor, if any.

Add a Key Binding

Now let's add a key binding to trigger the ascii-art:convert command. Open keymaps/ascii-art.json and add a key binding linking Alt+Ctrl+A to the ascii-art:convert command. You can delete the pre-existing key binding since you won't need it anymore.

When finished, the file should look like this:

{
  "atom-text-editor": {
    "ctrl-alt-a": "ascii-art:convert"
  }
}

Now reload the window and verify that the key binding works.

WARNING

Warning: The Atom keymap system is case-sensitive. This means that there is a distinction between a and A when creating keybindings. a means that you want to trigger the keybinding when you press A. But A means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a when you want to trigger the keybinding when you press Shift+A.

We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.

Add the ASCII Art

Now we need to convert the selected text to ASCII art. To do this we will use the figletopen in new window Node module from npmopen in new window. Open package.json and add the latest version of figlet to the dependencies:

"dependencies": {
  "figlet": "1.0.8"
}

After saving the file, run the command "Update Package Dependencies: Update" from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run "Update Package Dependencies: Update" whenever you update the dependencies field in your package.json file.

If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log file in your directory. That file should give you some idea as to what went wrong.

Now require the figlet node module in lib/ascii-art.js and instead of inserting "Hello, World!", convert the selected text to ASCII art.

convert () {
  const editor = atom.workspace.getActiveTextEditor()
  if (editor) {
    const selection = editor.getSelectedText()

    const figlet = require('figlet')
    const font = 'o8'
    figlet(selection, {font}, function (error, art) {
      if (error) {
        console.error(error)
      } else {
        editor.insertText(`\n${art}\n`)
      }
    })
  }
}

Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.

There are a couple of new things in this example we should look at quickly. The first is the editor.getSelectedText()open in new window which, as you might guess, returns the text that is currently selected.

We then call the Figlet code to convert that into something else and replace the current selection with it with the editor.insertText()open in new window call.

Summary

In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.

Package: Active Editor Info

We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Atom's UI—you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Atom will restore them the next time you open the project. This system is used by Atom's tree view, as well as by third party packages like Nuclideopen in new window for its console, debugger, outline view, and diagnostics (linter results).

For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/atom/active-editor-info.

Create the Package

To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Paletteopen in new window. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter active-editor-info as the name of the package.

Add an Opener

Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Atom. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open(), Atom will go through all of its openers until it finds one that can handle the URI you passed.

Let's open lib/active-editor-info.js and edit our activate() method to register an opener:

"use babel";

import ActiveEditorInfoView from "./active-editor-info-view";
import { CompositeDisposable, Disposable } from "atom";

export default {
	subscriptions: null,

	activate(state) {
		this.subscriptions = new CompositeDisposable(
			// Add an opener for our view.
			atom.workspace.addOpener((uri) => {
				if (uri === "atom://active-editor-info") {
					return new ActiveEditorInfoView();
				}
			}),

			// Register command that toggles this view
			atom.commands.add("atom-workspace", {
				"active-editor-info:toggle": () => this.toggle(),
			}),

			// Destroy any ActiveEditorInfoViews when the package is deactivated.
			new Disposable(() => {
				atom.workspace.getPaneItems().forEach((item) => {
					if (item instanceof ActiveEditorInfoView) {
						item.destroy();
					}
				});
			})
		);
	},

	deactivate() {
		this.subscriptions.dispose();
	},

	toggle() {
		console.log("Toggle it!");
	},
};

You'll notice we also removed the activeEditorInfoView property and the serialize() method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize() method. We'll come back to that later.

You probably also noticed that our toggle() implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:

  toggle() {
    atom.workspace.toggle('atom://active-editor-info');
  }

Updating the View

Atom uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:

  getTitle() {
    // Used by Atom for tab text
    return 'Active Editor Info';
  }

  getURI() {
    // Used by Atom to identify the view when toggling.
    return 'atom://active-editor-info';
  }

Now reload the window and run the "Active Editor Info: Toggle" command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.

Note

We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.

Constraining Our Item's Locations

The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:

  getDefaultLocation() {
    // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
    // Valid values are "left", "right", "bottom", and "center" (the default).
    return 'right';
  }

  getAllowedLocations() {
    // The locations into which the item can be moved.
    return ['left', 'right', 'bottom'];
  }

Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.

Show Active Editor Info

Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:

this.subscriptions = atom.workspace
	.getCenter()
	.observeActivePaneItem((item) => {
		if (!atom.workspace.isTextEditor(item)) {
			message.innerText = "Open a file to see important information about it.";
			return;
		}
		message.innerHTML = `
    <h2>${item.getFileName() || "untitled"}</h2>
    <ul>
      <li><b>Soft Wrap:</b> ${item.softWrapped}</li>
      <li><b>Tab Length:</b> ${item.getTabLength()}</li>
      <li><b>Encoding:</b> ${item.getEncoding()}</li>
      <li><b>Line Count:</b> ${item.getLineCount()}</li>
    </ul>
  `;
	});

Now whenever you open a text editor in the center, the view will update with some information about it.

WARNING

We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.

Also, don't forget to clean up the subscription in the destroy() method:

destroy() {
  this.element.remove();
  this.subscriptions.dispose();
}

Serialization

If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Atom how to serialize it yet. Let's do that now.

The first step is to implement a serialize() method on our ActiveEditorInfoView class. Atom will call the serialize() method on every item in the workspace periodically to save its state.

  serialize() {
    return {
      // This is used to look up the deserializer function. It can be any string, but it needs to be
      // unique across all packages!
      deserializer: 'active-editor-info/ActiveEditorInfoView'
    };
  }

Note

All of our view's state is derived from the active text editor so we only need the deserializer field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!

Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json file:

{
  "name": "active-editor-info",
  ...
  "deserializers": {
    "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
  }
}

Notice that the key ("active-editor-info/ActiveEditorInfoView") matches the string we used in our serialize() method above. The value ("deserializeActiveEditorInfoView") refers to a function in our main module, which we still need to add. Go back to active-editor-info.js and do that now:

  deserializeActiveEditorInfoView(serialized) {
    return new ActiveEditorInfoView();
  }

The value returned from our serialize() method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.

Reload Atom and toggle the view with the "Active Editor Info: Toggle" command. Then reload Atom again. Your view should be just where you left it!

Summary

In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!

Creating a Theme

Atom's interface is rendered using HTML, and it's styled via Lessopen in new window which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.

Atom supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.

Theme boundary

Themes can be installed and changed from the Settings View which you can open by selecting the Atom > PreferencesFile > PreferencesEdit > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.

Getting Started

Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:

  • Less is a superset of CSS, but it has some really handy features like variables. If you aren't familiar with its syntax, take a few minutes to familiarize yourselfopen in new window.
  • You may also want to review the concept of a package.json (as covered in Atom package.json). This file is used to help distribute your theme to Atom users.
  • Your theme's package.json must contain a theme key with a value of ui or syntax for Atom to recognize and load it as a theme.
  • You can find existing themes to install or fork in the atom.io themes registryopen in new window.

Creating a Syntax Theme

Let's create your first theme.

To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Syntax Theme" to generate a new theme package. Select "Generate Syntax Theme," and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax.

Tip

Tip: Syntax themes should end with -syntax and UI themes should end with -ui.

Atom will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with Cmd+,Ctrl+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.

Open up styles/colors.less to change the various color variables which have already been defined. For example, turn @red into #f4c2c1.

Then open styles/base.less and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.

As an example, let's make the .gutter background-color into @red.

Reload Atom by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R to see the changes you made reflected in your Atom window. Pretty neat!

Tip

Tip: You can avoid reloading to see changes you make by opening an Atom window in Dev Mode. To open a Dev Mode Atom window run atom --dev . in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!

Note

Note: It's advised to not specify a font-family in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.

Creating a UI Theme

To create a UI theme, do the following:

  1. Fork the ui-theme-templateopen in new window
  2. Clone the forked repository to the local filesystem
  3. Open a terminal in the forked theme's directory
  4. Open your new theme in a Dev Mode Atom window run atom --dev . in the terminal or use the View > Developer > Open in Dev Mode menu
  5. Change the name of the theme in the theme's package.json file
  6. Name your theme end with a -ui, for example super-white-ui
  7. Run apm link --dev to symlink your repository to ~/.atom/dev/packages
  8. Reload Atom using Alt+Cmd+Ctrl+LAlt+Ctrl+R
  9. Enable the theme via the "UI Theme" drop-down in the "Themes" tab of the Settings View
  10. Make changes! Since you opened the theme in a Dev Mode window, changes will be instantly reflected in the editor without having to reload.

Tip

Tip: Because we used apm link --dev in the above instructions, if you break anything you can always close Atom and launch Atom normally to force Atom to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.

Theme Variables

UI themes must provide a ui-variables.less and Syntax themes a syntax-variables.less file. It contains predefined variables that packages use to make sure the look and feel matches.

Here the variables with the default values:

These default values will be used as a fallback in case a theme doesn't define its own variables.

Use in Packages

In any of your package's .less files, you can access the theme variables by importing the ui-variables or syntax-variables file from Atom.

Your package should generally only specify structural styling, and these should come from the style guideopen in new window. Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!

Here's an example .less file that a package can define using theme variables:

@import "ui-variables";

.my-selector {
	background-color: @base-background-color;
	padding: @component-padding;
}
@import "syntax-variables";

.my-selector {
	background-color: @syntax-background-color;
}

Development workflow

There are a few tools to help make theme development faster and easier.

Live Reload

Reloading by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R after you make changes to your theme is less than ideal. Atom supports live updatingopen in new window of styles on Atom windows in Dev Mode.

To launch a Dev Mode window:

  • Open your theme directory in a dev window by selecting the View > Developer > Open in Dev Mode menu item
  • Or launch Atom from the terminal with atom --dev

If you'd like to reload all the styles at any time, you can use the shortcut Alt+Cmd+Ctrl+LAlt+Ctrl+R.

Developer Tools

Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the Alt+Cmd+ICtrl+Shift+I shortcut.

The dev tools allow you to inspect elements and take a look at their CSS properties.

Developer Tools

Check out Google's extensive tutorialopen in new window for a short introduction.

Atom Styleguide

If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The Styleguideopen in new window is a page that renders every component Atom supports.

To open the Styleguide, open the command palette with Cmd+Shift+PCtrl+Shift+P and search for "styleguide", or use the shortcut Cmd+Ctrl+Shift+GCtrl+Shift+G.

Style Guide

Side by side

Sometimes when creating a theme (or package) things can go wrong and the editor becomes un-usable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Atom in "normal" mode to fix the issue, it's advised to open two Atom windows. One for making changes and one in Dev Mode to see the changes getting applied.

Side by side screenshot

Make changes on the left, see the changes getting applied in "Dev Mode" on the right.

Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.

Publish your theme

Once you're happy with your theme and would like to share it with other Atom users, it's time to publish it. 🎉

Follow the steps on the Publishing page. The example used is for the Word Count package, but publishing a theme works exactly the same.

Creating a Grammar

Atom's syntax highlighting and code folding system is powered by Tree-sitteropen in new window. Tree-sitter parsers create and maintain full syntax treesopen in new window representing your code.

This syntax tree gives Atom a comprehensive understanding of the structure of your code, which has several benefits:

  1. Syntax highlighting will not break because of formatting changes.
  2. Code folding will work regardless of how your code is indented.
  3. Editor features can operate on the syntax tree. For instance, the Select Larger Syntax Node and Select Smaller Syntax Node allow you to select conceptually larger and smaller chunks of your code.
  4. Community packages can use the syntax tree to manipulate code intelligently.

Tree-sitter grammars are relatively new. Many languages in Atom are still supported by TextMate grammars, though we intend to phase these out over time.

If you're adding support for a new language, you're in the right place!

Getting Started

There are two components required to use Tree-sitter in Atom: a parser and a grammar file.

The Parser

Tree-sitter generates parsers based on context-free grammarsopen in new window that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Atom.

They can also be developed and tested at the command line, separately from Atom. Tree-sitter has its own documentation pageopen in new window on how to create these parsers. The Tree-sitter GitHub organizationopen in new window also contains a lot of example parsers that you can learn from, each in its own repository.

Once you have created a parser, you need to publish it to the NPM registryopen in new window to use it in Atom. To do this, make sure you have a name and version in your parser's package.json:

{
  "name": "tree-sitter-mylanguage",
  "version": "0.0.1",
  // ...
}

then run the command npm publish.

The Package

Once you have a Tree-sitter parser that is available on npm, you can use it in your Atom package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json, a grammars subdirectory, and a single json or cson file in the grammars directory, which can be named anything.

language-mylanguage
├── LICENSE
├── README.md
├── grammars
│   └── mylanguage.cson
└── package.json

The Grammar File

The mylanguage.cson file specifies how Atom should use the parser you created.

Basic Fields

It starts with some required fields:

name: 'My Language'
scopeName: 'mylanguage'
type: 'tree-sitter'
parser: 'tree-sitter-mylanguage'
  • scopeName - A unique, stable identifier for the language. Atom users will use this in configuration files if they want to specify custom configuration based on the language.
  • name - A human readable name for the language.
  • parser - The name of the parser node module that will be used for parsing. This string will be passed directly to require()open in new window in order to load the parser.
  • type - This should have the value tree-sitter to indicate to Atom that this is a Tree-sitter grammar and not a TextMate grammar.

Language Recognition

Next, the file should contain some fields that indicate to Atom when this language should be used. These fields are all optional.

  • fileTypes - An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.
  • firstLineRegex - A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.
  • contentRegex - A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex matches, this grammar will be preferred over another grammar with no contentRegex. If the contentRegex does not match, a grammar with no contentRegex will be preferred over this one.

Syntax Highlighting

The HTML classes that Atom uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.

Here is a simple example:

scopes:
  'call_expression > identifier': 'entity.name.function'

This entry means that, in the syntax tree, any identifier node whose parent is a call_expression should be highlighted using three classes: syntax--entity, syntax--name, and syntax--function.

Note that in this selector, we're using the immediate child combinatoropen in new window (>). Arbitrary descendant selectors without this combinator (for example 'call_expression identifier', which would match any identifier occurring anywhere within a call_expression) are currently not supported.

Advanced Selectors

The keys of the scopes object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:

scopes:
  '''
  function_declaration > identifier,
  call_expression > identifier,
  call_expression > field_expression > field_identifier
  ''': 'entity.name.function'

You can use the :nth-child pseudo-classopen in new window to select nodes based on their order within their parent. For example, this example selects identifier nodes which are the fourth (zero-indexed) child of a singleton_method node.

scopes:
  'singleton_method > identifier:nth-child(3)': 'entity.name.function'

Finally, you can use double-quoted strings in the selectors to select anonymous tokens in the syntax tree, like ( and :. See the Tree-sitter documentationopen in new window for more information about named vs anonymous tokens.

scopes:
  '''
    "*",
    "/",
    "+",
    "-"
  ''': 'keyword.operator'
Text-based Mappings

You can also apply different classes to a syntax node based on its text. Here are some examples:

scopes:

  # Apply the classes `syntax--builtin` and `syntax--variable` to all
  # `identifier` nodes whose text is `require`.
  'identifier': {exact: 'require', scopes: 'builtin.variable'},

  # Apply the classes `syntax--type` and `syntax--integer` to all
  # `primitive_type` nodes whose text starts with `int` or `uint`.
  'primitive_type': {match: /^u?int/, scopes: 'type.integer'},

  # Apply the classes `syntax--builtin`, `syntax--class`, and
  # `syntax--name` to `constant` nodes with the text `Array`,
  # `Hash` and `String`. For all other `constant` nodes, just
  # apply the classes `syntax--class` and `syntax--name`.
  'constant': [
    {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},
    'class.name'
  ]

In total there are four types of values that can be associated with selectors in scopes:

  • Strings - Each class name in the dot-separated string will be prefixed with syntax-- and applied to the selected node.
  • Objects with the keys exact and scopes - If the node's text equals the exact string, the scopes string will be used as described above.
  • Objects with the keys match and scopes - If the node's text matches the match regex pattern, the scopes string will be used as described above.
  • Arrays - The elements of the array will be processed from beginning to end. The first element that matches the selected node will be used as describe above.
Specificity

If multiple selectors in the scopes object match a node, the node's classes will be decided based on the most specificopen in new window selector. Note that the exact and match rules do not affect specificity, so you may need to supply the same exact or match rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:

scopes:
  'call_expression > identifier': 'entity.name.function'

  # If we did not include the second selector here, then this rule
  # would not apply to identifiers inside of call_expressions,
  # because the selector `call_expression > identifier` is more
  # specific than the selector `identifier`.
  'identifier, call_expression > identifier': [
    {exact: 'require', scopes: 'builtin.variable'},
    {match: '^[A-Z]', scopes: 'constructor'},
  ]

Language Injection

Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex - a regex pattern that will be tested against the language name provided by the injection point.

For example, in JavaScript, tagged template literalsopen in new window sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:

// HTML in a template literal
const htmlContent = html`<div>Hello ${name}</div>`;

The tree-sitter-javascript parser parses this tagged template literal as a call_expression with two children: an identifier and a template_literal:

(call_expression
  (identifier)
  (template_literal
    (interpolation
      (identifier))))

Here is an injection point that would allow syntax highlighting inside of template literals:

atom.grammars.addInjectionPoint("source.js", {
	type: "call_expression",

	language(callExpression) {
		const { firstChild } = callExpression;
		if (firstChild.type === "identifier") {
			return firstChild.text;
		}
	},

	content(callExpression) {
		const { lastChild } = callExpression;
		if (lastChild.type === "template_string") {
			return lastChild;
		}
	},
});

The language callback would then be called with every call_expression node in the syntax tree. In the example above, it would retrieve the first child of the call_expression, which is an identifier with the name "html". The callback would then return the string "html".

The content callback would then be called with the same call_expression node and return the template_string node within the call_expression node.

In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex:

injectionRegex: 'html|HTML'

Code Folding

The next field in the grammar file, folds, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:

folds: [

  # All `comment` nodes are foldable. By default, the fold starts at
  # the end of the node's first line, and ends at the beginning
  # of the node's last line.
  {
    type: 'comment'
  }

  # `if_statement` nodes are foldable if they contain an anonymous
  # "then" token and either an `elif_clause` or `else_clause` node.
  # The fold starts at the end of the "then" token and ends at the
  # `elif_clause` or `else_clause`.
  {
    type: 'if_statement',
    start: {type: '"then"'}
    end: {type: ['elif_clause', 'else_clause']}
  }

  # Any node that starts with an anonymous "(" token and ends with
  # an anonymous ")" token is foldable. The fold starts after the
  # "(" and ends before the ")".
  {
    start: {type: '"("', index: 0},
    end: {type: '")"', index: -1}
  }
]

Fold patterns can have one or more of the following fields:

  • type - A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.
  • start - An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields:
    • type - A string or array of strings. To start a fold, a child node's type must match one of these strings.
    • index - a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1 means the last child.
  • end - An object that is used to identify a child node before which the fold should end. It has the same structure as the start object.

Comments

The last field in the grammar file, comments, controls the behavior of Atom's Editor: Toggle Line Comments command. Its value is an object with a start field and an optional end field. The start field is a string that should be prepended to or removed from lines in order to comment or un-comment them.

In JavaScript, it looks like this:

comments:
  start: '// '

The end field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.

In CSS, it would look like this:

comments:
  start: '/* '
  end: ' */'

Example Packages

More examples of all of these features can be found in the Tree-sitter grammars bundled with Atom:

Creating a Legacy TextMate Grammar

Atom's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node command.

This section describes the Atom's legacy support for TextMate grammars.

TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.

Note

Note: This tutorial is a work in progress.

Getting Started

TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Atom uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:

  • https://www.regular-expressions.info/tutorial.html provides a comprehensive regex tutorial
  • https://www.rexegg.com/regex-quickstart.html contains a cheat sheet for various regex expressions
  • https://regex101.com/ or https://regexr.com/ allows live prototyping
  • https://github.com/kkos/oniguruma/blob/master/doc/RE the docs for the Oniguruma regex engine

Grammar files are written in the CSONopen in new window or JSONopen in new window format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.

Create the Package

To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual.

Tip

Tip: Grammar packages should start with language-.

The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the keymaps, lib, menus, and styles folders. Furthermore, in package.json, remove the activationCommands section. Now create a new folder called grammars, and inside that a file called flight-manual.cson. This is the main file that we will be working with - start by populating it with a boilerplate templateopen in new window. Now let's go over what each key means.

scopeName is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript's scopeName is source.js and language-html's is text.html.basic. Name it source.flight-manual for now.

name is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual.

fileTypes is an array of filetypes that language-flight-manual should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md extension to the list and remove the others.

patterns contains the array of regex patterns that will determine how the file is tokenized.

Adding Patterns

To start, let's add a basic pattern to tokenize the words Flight Manual whenever they show up. Your regex should look like \bFlight Manual\b. Here's what your patterns block should look like:

'patterns': [
  {
    'match': '\\bFlight Manual\\b'
    'name': 'entity.other.flight-manual'
  }
]

match is where your regex is contained, and name is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in Section 12.4 of the TextMate Manualopen in new window.

Tip

Tip: All scopes should end with the portion of the root scopeName after the leading source or text. In our case, all scopes should end with flight-manual.

Note

Note: Astute readers may have noticed that the \b was changed to \\b with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.

But what if we wanted to apply different scopes to Flight and Manual? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture property. For example:

'match': '\\b(Flight) (Manual)\\b'
'name': 'entity.other.flight-manual'
'captures':
  '1':
    'name': 'keyword.other.flight.flight-manual'
  '2':
    'name': 'keyword.other.manual.flight-manual'

This will assign the scope keyword.other.flight.flight-manual to Flight, keyword.other.manual.flight-manual to Manual, and entity.other.flight-manual to the overarching Flight Manual.

Begin/End Patterns

Now let's say we want to tokenize the ::: note Note blocks that occur in Flight Manual files. Our previous two examples used match, but one limit of match is that it can only match single lines. ::: note Note blocks, on the other hand, can span multiple lines. For these cases, you can use the begin/end keys. Once the regex in the begin key is matched, tokenization will continue until the end pattern is reached.

'begin': '({{)(#note)(}})'
'beginCaptures':
  '0': # The 0 capture contains the entire match
    'name': 'meta.block.start.flight-manual'
  '1':
    'name': 'punctuation.definition.block.flight-manual'
  '2':
    'name': 'keyword.note.flight-manual'
  '3':
    'name': 'punctuation.definition.block.flight-manual'
'end': '({{)(/note)(}})'
'endCaptures':
  '0':
    'name': 'meta.block.end.flight-manual'
  '1':
    'name': 'punctuation.definition.block.flight-manual'
  '2':
    'name': 'keyword.note.flight-manual'
  '3':
    'name': 'punctuation.definition.block.flight-manual'
'name': 'meta.block.note.flight-manual'

Tip

Tip: Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!

Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md file, you may have noticed that Flight Manual doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!

'begin': '({{)(#note)(}})'
'beginCaptures':
  '0': # The 0 capture contains the entire match
    'name': 'meta.block.start.flight-manual'
  '1':
    'name': 'punctuation.definition.block.flight-manual'
  '2':
    'name': 'keyword.note.flight-manual'
  '3':
    'name': 'punctuation.definition.block.flight-manual'
'end': '({{)(/note)(}})'
'endCaptures':
  '0':
    'name': 'meta.block.end.flight-manual'
  '1':
    'name': 'punctuation.definition.block.flight-manual'
  '2':
    'name': 'keyword.note.flight-manual'
  '3':
    'name': 'punctuation.definition.block.flight-manual'
'name': 'meta.block.note.flight-manual'
'patterns': [
  {
    'match': '\\b(Flight) (Manual)\\b'
    'name': 'entity.other.flight-manual'
    'captures':
      '1':
        'name': 'keyword.other.flight.flight-manual'
      '2':
        'name': 'keyword.other.manual.flight-manual'
  }
]

There. With the patterns block, Flight Manual should now receive the proper scopes.

Repositories and the Include keyword, or how to avoid duplication

At this point, note blocks are looking pretty nice, as is the Flight Manual keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include keyword comes in. include allows you to include other patterns, even from other grammars! language-gfm's scopeName is source.gfm, so let's include that. Our patterns block should now look like the following:

'patterns': [
  {
    'include': 'source.gfm'
  }
  {
    # Flight Manual pattern
  }
  {
    # Note begin/end pattern
  }
]

However, including source.gfm has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note pattern as well. Therefore, include helpfully recognizes the special $self scope. $self automatically includes all the top-level patterns of the current grammar. The note block can then be simplified to the following:

'begin': '({{)(#note)(}})'
# beginCaptures
'end': '({{)(/note)(}})'
# endCaptures
'name': 'meta.block.note.flight-manual'
'patterns': [
  {
    'include': '$self'
  }
]

Where to Go from Here

There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).

Publishing

Atom bundles a command line utility called apm which we first used back in Command Line to search for and install packages via the command line. The apm command can also be used to publish Atom packages to the public registry and update them.

Prepare Your Package

There are a few things you should double check before publishing:

  • Your package.json file has name, description, and repository fields.
  • Your package.json file has a version field with a value of "0.0.0".
  • Your package.json file has an engines field that contains an entry for Atom such as: "engines": {"atom": ">=1.0.0 <2.0.0"}.
  • Your package has a README.md file at the root.
  • Your repository URL in the package.json file is the same as the URL of your repository.
  • Your package is in a Git repository that has been pushed to GitHubopen in new window. Follow this guideopen in new window if your package isn't already on GitHub.

Publish Your Package

Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to the atom.io package registryopen in new window. You can do that by visiting https://atom.io/packages/your-package-name to see if the package already exists. If it does, update your package's name to something that is available before proceeding.

Now let's review what the apm publish command does:

  1. Registers the package name on atom.io if it is being published for the first time.
  2. Updates the version field in the package.json file and commits it.
  3. Creates a new Git tagopen in new window for the version being published.
  4. Pushes the tag and current branch up to GitHub.
  5. Updates atom.io with the new version being published.

Now run the following commands to publish your package:

$ cd path-to-your-package
$ apm publish minor

If this is the first package you are publishing, the apm publish command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a personal access tokenopen in new window in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your keychainopen in new window once you login.

Your package is now published and available on atom.io. Head on over to https://atom.io/packages/your-package-name to see your package's page.

With apm publish, you can bump the version and publish by using

$ apm publish <em>version-type</em>

where version-type can be major, minor and patch.

The major option to the publish command tells apm to increment the first number of the version before publishing so the published version will be 1.0.0 and the Git tag created will be v1.0.0.

The minor option to the publish command tells apm to increment the second number of the version before publishing so the published version will be 0.1.0 and the Git tag created will be v0.1.0.

The patch option to the publish command tells apm to increment the third number of the version before publishing so the published version will be 0.0.1 and the Git tag created will be v0.0.1.

Use major when you make a change that breaks backwards compatibility, like changing defaults or removing features. Use minor when adding new functionality or options, but without breaking backwards compatibility. Use patch when you've changed the implementation of existing features, but without changing the behaviour or options of your package. Check out semantic versioningopen in new window to learn more about best practices for versioning your package releases.

You can also run apm help publish to see all the available options and apm help to see all the other available commands.

Iconography

Atom comes bundled with the Octicons 4.4.0open in new window icon set. Use them to add icons to your packages.

NOTE: Some older icons from version 2.1.2 are still kept for backwards compatibility.

Overview

In the Styleguide under the "Icons" section you'll find all the Octicons that are available.

Octicons in the Styleguide

Usage

Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-.

As an example, to add a monitor icon (device-desktop), use the icon icon-device-desktop classes:

<span class="icon icon-device-desktop"></span>

Size

Octicons look best with a font-size of 16px. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px, 48px etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.

Usability

Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a tooltipopen in new window that appears on hover. Or a more subtle title="label" attribute would help as well.

Debugging

Atom provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when submitting issuesopen in new window:

Update to the Latest Version

You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using.

If you're using a released version, check which version of Atom you're using:

$ atom --version
> Atom    : 1.8.0
> Electron: 0.36.8
> Chrome  : 47.0.2526.110
> Node    : 5.1.1

Then check for the latest Stable versionopen in new window.

If you're building Atom from source, pull down the latest version of master and re-buildopen in new window.

Using Safe Mode

A large part of Atom's functionality comes from packages you can install. Atom will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.

To determine if that is happening, start Atom from the terminal in safe mode:

$ atom --safe

This starts Atom, but does not load packages from ~/.atom/packages or ~/.atom/dev/packages and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.

If removing or commenting out all content from the init script and starting Atom normally still produces the error, then try figuring out which package is causing trouble. Start Atom normally again and open the Settings View with Cmd+,Ctrl+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Atom or reload Atom with Alt+Cmd+Ctrl+LCtrl+Shift+F5 after you disable each package to make sure it's completely gone.

When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.

Clearing Saved State

Atom saves a number of things about your environment when you exit in order to restore Atom to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Atom from working properly. In these cases, you may want to clear the state that Atom has saved.

DANGER

:rotatinglight: Danger: Clearing the saved state permanently destroys any state that Atom has saved _across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.

Clearing the saved state can be done by opening a terminal and executing:

$ atom --clear-window-state

Reset to Factory Defaults

In some cases, you may want to reset Atom to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:

Once that is complete, you can launch Atom as normal. Everything will be just as if you first installed Atom.

Tip

Tip: The command given above doesn't delete the old configuration, just puts it somewhere that Atom can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the ~/.atom-backup%USERPROFILE%\.atom-backup directory.

Check for Linked Packages

If you develop or contribute to Atom packages, there may be left-over packages linked to your ~/.atom/packages or ~/.atom/dev/packages directories. You can use the apm links command to list all linked packages:

$ apm links
> /Users/octocat/.atom/dev/packages (0)
> └── (no links)
> /Users/octocat/.atom/packages (1)
> └── color-picker -> /Users/octocat/github/color-picker

You can remove links using the apm unlink command:

$ apm unlink color-picker
> Unlinking /Users/octocat/.atom/packages/color-picker ✓

See apm links --help and apm unlink --help for more information on these commands.

Tip

Tip: You can also use apm unlink --all to easily unlink all packages and themes.

Check for Incompatible Packages

If you have packages installed that use native Node modules, when you upgrade to a new version of Atom, they might need to be rebuilt. Atom detects this and through the incompatible-packages packageopen in new window displays an indicator in the status bar when this happens.

Incompatible Packages Status Bar Indicator

If you see this indicator, click it and follow the instructions.

Check Atom and Package Settings

In some cases, unexpected behavior might be caused by settings in Atom or in one of the packages.

Open Atom's Settings Viewopen in new window with Cmd+,Ctrl+,, the Atom > PreferencesFile > PreferencesEdit > Preferences menu option, or the "Settings View: Open" command from the Command Paletteopen in new window.

Settings View

Check Atom's settings in the Settings View, there's a description of most configuration options in the Basic Customization section. For example, if you want Atom to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.

Some of these options are also available on a per-language basis which means that they may be different for specific languages, for example JavaScript or Python. To check the per-language settings, open the settings for the language package under the Packages tab in the Settings View, for example the language-javascript or language-python package.

Since Atom ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the Wrap Guide packageopen in new window. And if you don't like it when Atom strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the whitespace package'sopen in new window settings.

Package Settings

Check Your Configuration

You might have defined some custom styles, keymaps or snippets in one of your configuration files. In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Atom.

Check Your Keybindings

If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Atom ships with the Keybinding Resolveropen in new window, a neat package which helps you understand what key Atom saw you press and the command that was triggered because of it.

Show the keybinding resolver with Cmd+.Ctrl+. or with "Keybinding Resolver: Show" from the Command palette. With the Keybinding Resolver shown, press a key combination:

Keybinding Resolver

The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:

  • the command for the keybinding
  • the CSS selector used to define the context in which the keybinding is valid
  • the file in which the keybinding is defined

The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.

If multiple keybindings are matched, Atom determines which keybinding will be executed based on the specificity of the selectors and the order in which they were loaded. If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:

  • The key combination was not used in the context defined by the keybinding's selector

    For example, you can't trigger the keybinding for the tree-view:add-file command if the Tree View is not focused.

  • There is another keybinding that took precedence

    This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.

Atom loads core Atom keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your keymap.cson file to tweak the keybindings and sort out problems like these. See the Keymaps in Depth section for more information.

If you notice that a package's keybindings are taking precedence over core Atom keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact atom maintainers on Atom's github discussionsopen in new window

Check Font Rendering Issues

You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press Alt+Cmd+ICtrl+Shift+I. Once the Developer Tools are open, click the "Elements" tab. Use the standard tools for finding the elementopen in new window containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:

Fonts In Use

Check for Errors in the Developer Tools

When an unexpected error occurs in Atom, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:

Exception Notification

Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press Alt-Cmd-ICtrl-Shift-I to open developer tools and then click the Console tab:

DevTools Error

If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.

Note

Note: When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.

Find Crash Logs

Diagnose Startup Performance

If Atom is taking a long time to start, you can use the Timecop packageopen in new window to get insight into where Atom spends time while loading.

Timecop

Timecop displays the following information:

  • Atom startup times
  • File compilation times
  • Package loading and activation times
  • Theme loading and activation times

If a specific package has high load or activation times, you might consider reporting an Issue to the maintainers. You can also disable the package to potentially improve future startup times.

Diagnose Runtime Performance

If you're experiencing performance problems in a particular situation, your Issue reportsopen in new window will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.

To run a profile, open the Developer Tools with Alt+Cmd+ICtrl+Shift+I. From there:

  1. Click the Profiles tab
  2. Select "Collect JavaScript CPU Profile"
  3. Click "Start"

DevTools Profiler

Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.

DevTools Profiler

To learn more, check out the Chrome documentation on CPU profilingopen in new window.

Profiling Startup Performance

If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup command line flag when starting Atom:

$ atom --profile-startup .

This will automatically capture a CPU profile as Atom is loading and open the Developer Tools once Atom loads. From there:

  1. Click the Profiles tab in the Developer Tools
  2. Select the "startup" profile
  3. Click the "Save" link for the startup profile

You can then include the startup profile in any Issue you report.

Check Your Build Tools

If you are having issues installing a package using apm install, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run apm install --check to see if the Atom package manager can build native code on your machine.

Check out the pre-requisites in the build instructionsopen in new window for your platform for more details.

Check if your GPU is causing the problem

If you encounter flickering or other rendering issues, you can stop Atom from using your Graphics Processing Unit (GPU) with the --disable-gpu Chromium flag to see if the fault lies with your GPU:

$ atom --disable-gpu

Chromium (and thus Atom) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu tells Atom to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage as the CPU has to work harder to do the things the GPU is optimized for.

Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization and --force-gpu-rasterization:

$ atom --enable-gpu-rasterization --force-gpu-rasterization

--enable-gpu-rasterization allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).

Be sure to use Chromium flags at the end of the terminal call if you want to use other Atom flags as they will not be executed after the Chromium flags e.g.:

$ atom --safe --enable-gpu-rasterization --force-gpu-rasterization

Writing Specs

We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?

Atom uses Jasmineopen in new window as its spec framework. Any new functionality should have specs to guard against regressions.

Create a New Spec

Atom specsopen in new window and package specsopen in new window are added to their respective spec directory. The example below creates a spec for Atom core.

Create a Spec File

Spec files must end with -spec so add sample-spec.coffee to the spec directory.

Add One or More describe Methods

The describe method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when; if it is more like a unit test it begins with the method name.

describe("when a test is written", function () {
	// contents
});

or

describe("Editor::moveUp", function () {
	// contents
});
Add One or More it Methods

The it method also takes two arguments, a description and a function. Try and make the description flow with the it method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".

describe("when a test is written", function () {
	it("has some expectations that should pass", function () {
		// Expectations
	});
});
Add One or More Expectations

The best way to learn about expectations is to read the Jasmine documentationopen in new window about them. Below is a simple example.

describe("when a test is written", function () {
	it("has some expectations that should pass", function () {
		expect("apples").toEqual("apples");
		expect("oranges").not.toEqual("apples");
	});
});
Custom Matchers

In addition to the Jasmine's built-in matchers, Atom includes the following:

  • jasmine-jqueryopen in new window
  • The toBeInstanceOf matcher is for the instanceof operator
  • The toHaveLength matcher compares against the .length property
  • The toExistOnDisk matcher checks if the file exists in the filesystem
  • The toHaveFocus matcher checks if the element currently has focus
  • The toShow matcher tests if the element is visible in the dom

These are defined in spec/spec-helper.coffeeopen in new window.

Asynchronous Specs

Writing Asynchronous specs can be tricky at first. Some examples.

Promises

Working with promises is rather easy in Atom. You can use our waitsForPromise function.

describe("when we open a file", function () {
	it("should be opened in an editor", function () {
		waitsForPromise(function () {
			atom.workspace
				.open("c.coffee")
				.then((editor) => expect(editor.getPath()).toContain("c.coffee"));
		});
	});
});

This method can be used in the describe, it, beforeEach and afterEach functions.

describe("when we open a file", function () {
	beforeEach(function () {
		waitsForPromise(() => atom.workspace.open("c.coffee"));
	});

	it("should be opened in an editor", function () {
		expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
			"c.coffee"
		);
	});
});

If you need to wait for multiple promises use a new waitsForPromise function for each promise. (Caution: Without beforeEach this example will fail!)

describe("waiting for the packages to load", function () {
	beforeEach(function () {
		waitsForPromise(() => atom.workspace.open("sample.js"));

		waitsForPromise(() => atom.packages.activatePackage("tabs"));

		waitsForPromise(() => atom.packages.activatePackage("tree-view"));
	});

	it("should have waited long enough", function () {
		expect(atom.packages.isPackageActive("tabs")).toBe(true);
		expect(atom.packages.isPackageActive("tree-view")).toBe(true);
	});
});

waitsForPromise can take an additional object argument before the function. The object can have the following properties:

  • shouldReject Whether the promise should reject or resolve (default: false)
  • timeout The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000)
  • label The label to display if promise times out (default: 'promise to be resolved or rejected')
describe("when we open a file", function () {
	it("should be opened in an editor", function () {
		waitsForPromise(
			{
				shouldReject: false,
				timeout: 5000,
				label: "promise to be resolved or rejected",
			},
			() =>
				atom.workspace
					.open("c.coffee")
					.then((editor) => expect(editor.getPath()).toContain("c.coffee"))
		);
	});
});
Asynchronous Functions with Callbacks

Specs for asynchronous functions can be done using the waitsFor and runs functions. A simple example.

describe("fs.readdir(path, cb)", function () {
	it("is async", function () {
		const spy = jasmine.createSpy("fs.readdirSpy");
		fs.readdir("/tmp/example", spy);

		waitsFor(() => spy.callCount > 0);

		runs(function () {
			const exp = [null, ["example.coffee"]];

			expect(spy.mostRecentCall.args).toEqual(exp);
			expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
		});
	});
});

For a more detailed documentation on asynchronous tests please visit the Jasmine documentationopen in new window.

Running Specs

Most of the time you'll want to run specs by triggering the window:run-package-specs command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec directory.

To run a limited subset of specs use the fdescribe or fit methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:

describe("when a test is written", function () {
	fit("has some expectations that should pass", function () {
		expect("apples").toEqual("apples");
		expect("oranges").not.toEqual("apples");
	});
});
Running on CI

It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packagesopen in new window and AppVeyor CI For Your Packagesopen in new window posts for more details.

Running via the Command Line

To run tests on the command line, run Atom with the --test flag followed by one or more paths to test files or directories. You can also specify a --timeout option, which will force-terminate your tests after a certain number of seconds have passed.

atom --test --timeout 60 ./test/test-1.js ./test/test-2.js

Customizing your test runner

WARNING

Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.

By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner field in your package.json. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.

Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:

  • testPaths An array of paths to tests to run. Could be paths to files or directories.
  • buildAtomEnvironment A function that can be called to construct an instance of the atom global. No atom global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters:
    • applicationDelegate An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate for a default instance. You can override specific methods on this object to prevent or test these interactions.
    • window A window global.
    • document A document global.
    • configDirPath A path to the configuration directory (usually ~/.atom).
    • enablePersistence A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false.
  • buildDefaultApplicationDelegate A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate parameter to buildAtomEnvironment.
  • logFile An optional path to a log file to which test output should be logged.
  • headless A boolean indicating whether or not the tests are being run from the command line via atom --test.
  • legacyTestRunner This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.

Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.

Handling URIs

Beginning in Atom 1.23, packages have the ability to handle special URIs triggered from the system; for example, a package named my-package can register itself to handle any URI starting with atom://my-package/.

WARNING

Warning: Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.

Any package with a URI handler that we feel violates this guideline is subject to removal from the Atom package registry at our discretion.

Modifying your package.json

The first step to handling URIs from your package is to modify its package.json file. You should add a new key called uriHandler, and its value should be an object.

The uriHandler object must contain a key called method with a string value that tells Atom which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation which can be set to the boolean false to prevent Atom from deferring activation of your package — see more below.

For example, if we want our package my-package to handle URIs with a method on our package's main module called handleURI, we could add the following to our package.json:

"uriHandler": {
  "method": "handleURI"
}

Modifying your Main Module

Now that we've told Atom that we want our package to handle URIs beginning with atom://my-package/ via our handleURI method, we need to actually write this method. Atom passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, parsed with Node's url.parse(uri, true)open in new window. The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.

Here's a sample package, written in JavaScript, that handles URIs with the package.json configuration we saw above.

export default {
	activate() {
		// normal activation code here
	},

	handleURI(parsedUri) {
		console.log(parsedUri);
	},
};

When Atom handles, for example, the URI atom://my-package/my/test/url?value=42&other=false, the package would log out something like the following:

{
  protocol: 'atom:',
  slashes: true,
  auth: null,
  host: 'my-package',
  port: null,
  hostname: 'my-package',
  hash: null,
  search: '?value=true&other=false',
  query: { value: '42', other: 'false' },
  pathname: '/my/test/url',
  path: '/my/test/url?value=true&other=false',
  href: 'atom://my-package/my/test/url?value=true&other=false'
}

Notice that the query string arguments are available in the query property, but are strings — you'll have to convert to other native types yourself.

Controlling Activation Deferral

For performance reasons, adding a uriHandler entry to your package's package.json will enable deferred activation. This means that Atom will not activate your package until it has a URI for it to handle — it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false to the URI handler config. For example,

"uriHandler": {
  "method": "handleURI",
  "deferActivation": false
}

Before doing this, make sure your package actually needs to be activated immediately — disabling deferred activation means Atom takes longer to start since it has to activate all packages without deferred activation.

Linux Support

Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Atom on Linux. If you want to configure URI handling on your system yourself, then you should configure atom: protocol URI's to trigger atom with the --uri-handler flag; for example, the URI atom://test/uri should launch Atom via atom --uri-handler atom://test/uri.

Core URIs

Atom provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>

Cross-Platform Compatibility

Atom runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.

File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).

Also consider:

  • Symlinks committed to Git will not checkout correctly on Windows - dynamically create what you need with fs.symlink instead
  • Symlinked directories are only available to Administrators on Windows - avoid a dependency on them

Filenames

  • Reserved filenames on Windows are com1-com9, lpt1-lpt9, con, nul, aux and prn (regardless of extension, e.g. prn.txt is disallowed)
  • Reserved characters on Windows are ? \ / < > ? % | : " so avoid where possible
  • Names with spaces when passed to the command line;
    • Windows requires you surround the path with double quotes e.g. "c:\my test"
    • macOS and Linux require a backslash before each space e.g. /my\ test

File paths

  • Windows uses \ although some tools and PowerShell allow / too
  • macOS and Linux use /

You can dynamically find out what your platform uses with path.sep or better yet use the node path library functions such as join and normalize which automatically take care of this.

Windows supports up to 250 characters for a path - avoid deeply nested directory structures

Paths are not URLs

URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.

  • Various characters are misinterpreted, e.g. ? as query string, # as a fragment identifier
  • Windows drive specifiers are incorrectly parsed as a protocol

If you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png

fs.stat on directories

The fs.stat function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.

path.relative can't traverse drives

  • On a macOS or Linux system path.relative can be used to calculate a relative path to traverse between any two given paths.
  • On Windows this is not always possible as it can contain multiple absolute roots, e.g. c:\ and d:\

Rapid file operations

Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider RimRAFopen in new window which has built-in retry logic for this.

Line endings

  • Windows uses CRLF
  • macOS and Linux use LF
  • Git on Windows often has autocrlf set which automatically converts between the two

If you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.

If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes e.g.

spec/fixtures/always-crlf.txt eol=crlf
spec/fixtures/always-lf.txt eol=lf
spec/fixtures/leave-as-is.txt -text

Converting from TextMate

It's possible that you have themes or grammars from TextMateopen in new window that you like and use and would like to convert to Atom. If so, you're in luck because there are tools to help with the conversion.

Converting a TextMate Grammar Bundle

Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Atom.

Let's convert the TextMate bundle for the Ropen in new window programming language. You can find other existing TextMate bundles on GitHubopen in new window.

You can convert the R bundle with the following command:

$ apm init --package language-r --convert https://github.com/textmate/r.tmbundle

You can now change directory into language-r to see the converted bundle. Once you link your package with the apm link command, your new package is ready to use. Launch Atom and open a .r file in the editor to see it in action!

Converting a TextMate Syntax Theme

This section will go over how to convert a TextMateopen in new window theme to an Atom theme.

Differences

TextMate themes use plistopen in new window files while Atom themes use CSSopen in new window or Lessopen in new window to style the UI and syntax in the editor.

The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Atom similarly.

Convert the Theme

Download the theme you wish to convert, you can browse existing TextMate themes on the TextMate websiteopen in new window.

Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme, you can convert the theme with the following command:

$ apm init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme

You can then change directory to my-theme to see the converted theme.

Activate the Theme

Once your theme is installed you can enable it by launching Atom and opening the Settings View with the Atom > PreferencesFile > PreferencesEdit > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.

Your theme is now enabled, open an editor to see it in action!

Hacking on Atom Core

If you're hitting a bug in Atom or just want to experiment with adding a feature to the core of the system, you'll want to run Atom in Dev Mode with access to a local copy of the Atom source.

Fork the atom/atom repository

Follow the GitHub Help instructions on how to fork a repoopen in new window.

Cloning and bootstrapping

Once you've set up your fork of the atom/atom repository, you can clone it to your local machine:

$ git clone git@github.com:<em>your-username</em>/atom.git

From there, you can navigate into the directory where you've cloned the Atom source code and run the bootstrap script to install all the required dependencies:

Running in Development Mode

Once you have a local copy of Atom cloned and bootstrapped, you can then run Atom in Development Mode. But first, if you cloned Atom to somewhere other than ~/github/atom%USERPROFILE%\github\atom you will need to set the ATOM_DEV_RESOURCE_PATH environment variable to point to the folder in which you cloned Atom. To run Atom in Dev Mode, use the --dev parameter from the terminal:

$ atom --dev <em>path-to-open</em>

Note

Note: If the atom command does not respond in the terminal, then try atom-dev or atom-beta. The suffix depends upon the particular source code that was cloned.

There are a couple benefits of running Atom in Dev Mode:

  1. When the ATOM_DEV_RESOURCE_PATH environment variable is set correctly, Atom is run using the source code from your local atom/atom repository. This means that you don't have to run script/buildscript\build every time you change code. Just restart Atom 👍
  2. Packages that exist in ~/.atom/dev/packages%USERPROFILE%\.atom\dev\packages are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.
  3. Packages that contain stylesheets, such as syntax themes, will have those stylesheets automatically reloaded by the dev-live-reloadopen in new window package. This does not live reload JavaScript or CoffeeScript files — you'll need to reload the window (window:reload) to see changes to those.

Running Atom Core Tests Locally

In order to run Atom Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH environment variable as mentioned above and then:

$ cd <em>path-to-your-local-atom-repo</em>
$ atom --test spec

Building

In order to build Atom from source, you need to have a number of other requirements and take additional steps.

Instructions
script/build Options
Troubleshooting

Contributing to Official Atom Packages

If you think you know which package is causing the issue you are reporting, feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the atom/atomopen in new window repository but be aware that it may get closed and reopened in the proper package's repository.

Hacking on Packages

Cloning

The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Atom in order to run apm install.

For example, if you want to make changes to the tree-view package, fork the repo on your github account, then clone it:

$ git clone git@github.com:<em>your-username</em>/tree-view.git

Next install all the dependencies:

$ cd tree-view
$ apm install
> Installing modules ✓

Now you can link it to development mode so when you run an Atom window with atom --dev, you will use your fork instead of the built in package:

$ apm link -d
Running in Development Mode

Editing a package in Atom is a bit of a circular experience: you're using Atom to modify itself. What happens if you temporarily break something? You don't want the version of Atom you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.

To open a development mode window, use the "Application: Open Dev" command. You can also run dev mode from the command line with atom --dev.

To load your package in development mode, create a symlink to it in ~/.atom/dev/packages. This occurs automatically when you clone the package with apm develop. You can also run apm link --dev and apm unlink --dev from the package directory to create and remove dev-mode symlinks.

Installing Dependencies

You'll want to keep dependencies up to date by running apm update after pulling any upstream changes.

Creating a Fork of a Core Package in atom/atom

Several of Atom's core packages are maintained in the packages directory of the atom/atom repositoryopen in new window. If you would like to use one of these packages as a starting point for your own package, please follow the steps below.

Tips

Tip: In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Atom package.

Creating Your New Package

For the sake of this guide, let's assume that you want to start with the current code in the one-light-uiopen in new window package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".

  1. Download the current contents of the atom/atom repository as a zip fileopen in new window

  2. Unzip the file to a temporary location (for example /tmp/atomC:\TEMP\atom)

  3. Copy the contents of the desired package into a working directory for your fork

    $ <span class='platform-mac platform-linux'>cp -R /tmp/atom/packages/one-light-ui ~/src/one-light-ui-plus</span><span class='platform-windows'>xcopy C:\TEMP\atom\packages\one-light-ui C:\src\one-light-ui-plus /E /H /K</span>
    
  4. Create a local repository and commit the initial contents

    $ cd ~/src/one-light-ui-plus
    $ git init
    $ git commit -am "Import core Atom package"
    
  5. Update the name property in package.json to give your package a unique name

  6. Make the other customizations that you have in mind

  7. Commit your changes

    $ git commit -am "Apply initial customizations"
    
  8. Create a public repository on github.comopen in new window for your new package

  9. Follow the instructions in the github.com UI to push your code to your new online repository

  10. Follow the steps in the Publishing guide to publish your new package

Merging Upstream Changes into Your Package

The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.

Maintaining a Fork of a Core Package in atom/atom

Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team consolidated many core Atom packagesopen in new window into the atom/atom repositoryopen in new window. For example, the one-light-ui package was originally maintained in the atom/one-light-uiopen in new window repository, but it is now maintained in the packages/one-light-ui directory in the atom/atom repositoryopen in new window.

If you forked one of the core packages before it was moved into the atom/atom repository, and you want to continue merging upstream changes into your fork, please follow the steps below.

Step-by-step guide

For the sake of this guide, let's assume that you forked the atom/one-light-uiopen in new window repository, renamed your fork to one-light-ui-plus, and made some customizations.

Add atom/atom as a Remote

Navigate to your local clone of your fork:

$ cd path/to/your/fork

Add the atom/atom repositoryopen in new window as a git remote:

$ git remote add upstream https://github.com/atom/atom.git
Get the Latest Changes for the Core Package

Tip

Tip: Follow these steps each time you want to merge upstream changes into your fork.

Fetch the latest changes from the atom/atom repository:

$ git fetch upstream

Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui directory:

$ git log upstream/master -- packages/one-light-ui
8ac9919a0 Bump up border size (Hugh Baht, 17 minutes ago)
3bf4d226e Remove obsolete build status link in one-light-ui README (Jason Rudolph, 3 days ago)
3edf64ad0 Merge pull request #42 from atom/sm-select-list (simurai, 2 weeks ago)
...

Look through the log and identify the commits that you want to merge into your fork.

Merge Upstream Changes into Your Fork

For each commit that you want to bring into your fork, use git format-patchopen in new window in conjunction with git amopen in new window. For example, to merge commit 8ac9919a0 into your fork:

$ git format-patch -1 --stdout 8ac9919a0 | git am -p3

Repeat this step for each commit that you want to merge into your fork.

Summary

If you finished this chapter, you should be an Atom-hacking master. We've discussed how you should work with CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.

Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Atom.

In the next chapter, we’ll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.