atom/atom — pull request #3469 by nathansobo (2014-09-05 23:08:19+00:00)
Title: Add command registry

/cc @benogle

## Tasks

* [x] Command dispatching
* [x] Assign `atom.commands` global
* [x] Test out some basic commands
* [ ] List commands registered via `atom.commands` and `$::command` in the
command palette
* [ ] Use command registry for all core view commands
* [ ] WorkspaceView
* [ ] PaneView
* [ ] EditorView
* [ ] Document `atom.commands`
* We should wait to deprecate `$::command` until our overall view strategy is
in place.

## Background

Currently, to register a command in Atom you are forced to use jQuery. When you call `::command` on a jQuery-wrapped DOM node, it registers a custom event listener based on the command name with `::on` and adds some metadata to the element with `::data`, which we read out later to display a list of available commands for the focused element in the command palette.

This approach has a couple major issues. First, it forces users to use our jQuery extension to register any command. Second, it forces users to get references to view instances when they could otherwise get by operating entirely on the model. If we're going to move away from requiring jQuery to build packages, we need a new approach for registering commands.

## Basics

Commands will be registered via a new global, `atom.commands`, and all events will be handled via *event delegation* rather than listening directly on specific view instances. Here's a basic example where we listen for increase/decrease font size commands on the workspace.

```coffee
disposable = atom.commands.add '.workspace',
'window:increase-font-size': -> @getModel().increaseFontSize()
'window:decrease-font-size': -> @getModel().decreaseFontSize()

# to remove these commands later...
disposable.dispose()
```

First comes the `.workspace` CSS selector, indicating we want to handle these commands when they bubble through the workspace element. Then comes a hash mapping command names to handlers. Each handler is invoked in the context of the DOM element that matches the selector.

Eventually these will be HTML 5 custom elements with domain-specific methods, but for now we extend the element with a `::getModel()` method so the workspace model can be accessed from the view. Jumping straight to the model will probably be the most common pattern, since we don't want to perform logic in the view.

To remove the added commands, just call `.dispose()` on the disposable that's returned from add.

## Bubbling Semantics

Command events begin at the event target and bubble upward. As the event bubbles through each node, event listeners associated with matching selectors are called in order of specificity. In the event of a specificity tie, the most recently added listeners are called first, just like in CSS. To cancel bubbling, call `.stopPropagation()` on the event as normal. You can also call `.stopImmediatePropagation()` to stop bubbling and also stop invoking additional listeners on the current element.

## Command Palette (In Progress)

The command registry has a `::findCommands` method, named to blend in with `::findKeyBindings` on the `KeymapManager`. You call it with an element to get a list of all commands that can be dispatched on that element.

```coffee
atom.commands.findCommands(element: document.activeElement)
### =>
[
{
name: 'window:increase-font-size'
displayName: 'Workspace: Increase Font Size'
selector: '.workspace'
},
{
name: 'window:decrease-font-size'
displayName: 'Workspace: Decrease Font Size'
selector: '.workspace'
}
...
]
###
```

## Backward Compatibility

The command registry can peacefully coexist with commands registered via our `$::command` method so long as `::findCommands` includes commands registered via jQuery in addition to those registered via the new model. We'll deprecate `$::command` and eventually remove it.

## Event Handling Strategy

The command registry currently listen for events with *capture semantics*, meaning it gets the first crack at all custom events. I'm not sure this is the best approach because it could interfere with listeners that are registered directly on elements, but on the other hand, this is sort of the point. It seemed like it might be a good idea so we can guarantee that people can always invoke commands they see in the command palette. With this approach, the command registry is always the source of truth.

## Future Possibilities

One issue we encounter is that people want to refer to a command in multiple ways when typing in the command palette. This is discussed in atom/command-palette#11. We could allow richer metadata to be provided instead of just a callback as follows.

```coffee
atom.commands.add '.workspace',
'window:increase-font-size':
listener: -> @getModel().increaseFontSize()
displayName: "Window: Make Fonts Bigger"
aliases: ["window:embiggen-fonts", "window:i-cant-read-that!"]
```

Just an idea. Don't want to do it in this PR.