Extending the Brackets Code Editor with JavaScript

brackets_extensions_header

By Alessandro Artoni

As web developers, we have all tried many different tools, IDE’s and text editors to improve our daily workflow. Apart from the old-fashioned vim/emacs lover, many of us currently use either Sublime Text (with its great collection of plugins) or WebStorm. Both have great features, plugins and support but there’s a new player that I believe is going to change things: Brackets

To summarize why I think Brackets, in a couple of quarter, is winning the web editors challange:

  1. Dude, it’s open source! Although developed and supported by Adobe, Brackets is completely open source (and, in my opinion, open is better).
  2. Brackets itself is a web app. It’s built with all the technologies we love: HTML, CSS and JavaScript runing in a Chromium shell. This means that, yes, if you’re a web developer, you can help improving it (by, for instance, writing an extension).
  3. Even if it’s relatively new, it is already pretty stable, with a lot of extensions that have been developed. It ships with some cool and unique features: Quick Edit, Live Developement, tern code intelligence, LESS and Sass support and more.

Ok. It’s nice. But perhaps you try it and find it’s missing <this feature>. Right, let’s add it, right now as an extension,

Always Start with Ponies

If the internet has taught us anything, It’s that there are never enough ponies and unicorns – and that certainly applies to Brackets as well. So, let’s add some sparkling happiness to our new web editor (many thanks to my dear friend Paolo Chillari for the topic suggestion).

Project set-up

The first thing to do (after installing Brackets, of course) is to locate the extension folder. Just go to Help > Show Extensions Folder from the menu. It should be something like ~/Library/Application Support/brackets/extensions on Mac or C:\Users\<Your Username>\AppData\Roaming\Brackets\extensions on Windows.

Go into the “user” subfolder and create a new project root which we’ll call “cornify-brackets”. What you need at this point are just two files:

  1. main.js: this is the file that will contains the JavaScript code for the extension.
  2. package.json: this file defines all the relevant metadata for our project (extension name, version number, repository, etc).

Let’s start with the latter:

{
    "name": "cornify-brackets",
    "title": "Cornify",
    "description": "add",
    "homepage": "http://artoale.com/tutorial/brackets/2013/09/30/writing-brackets-extension-01/",
    "version": "0.0.1",
    "author": "Alessandro Artoni <artoale@gmail.com> (https://github.com/artoale)",
    "license": "MIT",
    "engines": {
        "brackets": ">=0.31.0"
    }
}

The format is straightforward, especially for those who already have some experience with Node.js and npm since the format is almost exactly the same. All this information will become particularly useful for when we publish our extension.

Before we can start coding, you have to know that Brackets extensions use the AMD CommonJS Wrapper. Which means that your code will be wrapped in a callback passed to the global function. Inside that callback you’ll be passed in three objects: require, which is a function for managing your own dependencies, exports and module, to be used exclusively and allow you to expose functionality to the rest of the world.

Ok, that said, we’re ready to go.

Main.js structure

Let’s start with the definition of our plugin:

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window, Mustache */

define(function (require, exports, module) {
    "use strict";

    var AppInit = brackets.getModule("utils/AppInit");

    AppInit.appReady(function () {
        //...
    });
});

From this code you can already see some important concepts:

Globals

From within your code, you have access to some global variables:

  1. window: The browser global object. Since you’re a web developer, you’re probably already familiar with this.
  2. brackets: Used to access all Brackets specific functionalities and submodules (in particular the brackets.getModule function allows you to retrieve the module you need as defined in Brackets source code).
  3. Mustache: Brackets uses Mustache as a templating language, which means that you can use it in your extension as well.
  4. $ and jQueryjQuery (v2.0+) comes embedded within the editor to make your DOM manipulation easy
  5. define: we’ve already discussed that.

Plugin Initialization

When we write a piece of code that runs in the browser, we usually listen (directly or not) to the document.onload event to be sure that all the HTML has been parsed, the images are loaded and the DOM is ready. Since Brackets needs to do some more complicated stuff at startup, registering an event on the onload event is not safe aspart of the editor may not be loaded yet (RequireJS is used for loading modules and it is asyncronous) and you may not be able to access all the functionality you need. For that reason, the Brackets developers give us the utils/AppInit module. Our inizialization code should be passed as a callback to its appReady event (and not htmlReady, since that is fired before any plugins are loaded).

Let’s try it!

Yeah, but it does nothing…yet. Let’s fix that by adding a line to the appReady callback:

AppInit.appReady(function () {
    console.log("I'm a fabulous unicorn!");
});

Our v0.0.1 is ready to go! Restart Brackets via Debug > Reload Brackets or Cmd+R on Mac and open the dev tools to see the magic.

That’s right, I forgot to tell you, even the debugging tool your used to when writing web apps is available for Brackets, including Chromium Developer Tools. Just go on Debug > Show Developer Tools (Cmd+alt+I on Mac) and open the console tab. You should be able to see the “I’m a unicorn” text right there.

Nice, isn’t it? But, as you may have noticed…this doesn’t really look like a unicorn, sadly it’s just a sentence. Let’s improve that.

Loading Static Files with RequireJS

RequireJS and AMD in general are great for building structured, modular code, but there’s another, lesser-known feature: static file loading. It’s very useful when you need to load non-JavaScript resources like templates or, in our case unicorn ASCII-ART.

Start by adding a “lib” folder inside your project root and create a text file inside it (let’s call it “unicorn.txt”). Now we’re ready for some real goodness.

                                                    /
                                                  .7
                                       \       , //
                                       |\.--._/|//
                                      /\ ) ) ).'/
                                     /(  \  // /
                                    /(   J`((_/ \
                                   / ) | _\     /
                                  /|)  \  eJ    L
                                 |  \ L \   L   L
                                /  \  J  `. J   L
                                |  )   L   \/   \
                               /  \    J   (\   /
             _....___         |  \      \   \```
      ,.._.-'        '''--...-||\     -. \   \
    .'.=.'                    `         `.\ [ Y
   /   /                                  \]  J
  Y / Y                                    Y   L
  | | |          \                         |   L
  | | |           Y                        A  J
  |   I           |                       /I\ /
  |    \          I             \        ( |]/|
  J     \         /._           /        -tI/ |
   L     )       /   /'-------'J           `'-:.
   J   .'      ,'  ,' ,     \   `'-.__          \
    \ T      ,'  ,'   )\    /|        ';'---7   /
     \|    ,'L  Y...-' / _.' /         \   /   /
      J   Y  |  J    .'-'   /         ,--.(   /
       L  |  J   L -'     .'         /  |    /\
       |  J.  L  J     .-;.-/       |    \ .' /
       J   L`-J   L____,.-'`        |  _.-'   |
        L  J   L  J                  ``  J    |
        J   L  |   L                     J    |
         L  J  L    \                    L    \
         |   L  ) _.'\                    ) _.'\
         L    \('`    \                  ('`    \
          ) _.'\`-....'                   `-....'
         ('`    \
          `-.___/

Copy and paste the text above into the unicorn.txt file. Next we’ll load it into our JavaScript file by using the RequireJS function described earlier. In order to load static files with RequireJS we have to specify their format before the file path in the call to the require function. In our cas,e the string becomes text!lib/unicorn.txt - we’re loading a text file called unicorn.txt inside the lib folder relative to our extension root. Once you edit your main.js it should look something like this:

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window, Mustache */

define(function (require, exports, module) {
    "use strict";

    var AppInit = brackets.getModule('utils/AppInit'),
        unicornAscii = require('text!lib/unicorn.txt');

    AppInit.appReady(function () {
        console.log(unicornAscii);
    });
});

Save everything and Cmd+R again: you should now see a beautiful unicorn bringing happiness to the dev tools.

Adding a menu command

This is already pretty good as we’ve got a unicorn in our editor, but we can add more! Luckily, Brackets gives you the ability to create custom commands and add them to the main menu (and even to create new menus, if you have to). In order to do this we’ll need to utilize the CommandManager and Menus modules.

Both of these modules expose a lot of functions and variables to work with menus, but for now, we’ll just focus on what we need to add more unicorns (if you want a broader overview of what CommandManager offers in terms of anAPI, have a look at the “src/command” directory in the Brackets source code, and spot the exports).

For our purposes, we only need a couple of methods:

  1. CommandManager.register allow us to register a new command inside Brackets. This method requires, in this order, the command name used to create the menu item, a command identifier string (which should be unique) and a function that will be executed when your command is run.
  2. Menus.getMenu is used to retrieve the menu instance to which we want to add our command. Since adding ponies seems very helpful we’ll be using helpMenu.
  3. helpMenu.addMenuItem is, as you might imagine, the method we will use to add our command to the editor menu.

First, start with loading the right module from Brackets core and define a constant to identify our command:

var CommandManager = brackets.getModule("command/CommandManager"),
            Menus  = brackets.getModule("command/Menus"),
            CORNIFY_CMD_ID = "artoale.cornify";

Note that the ID I’ve picked is scoped, in order to avoid name collision with other extensions.

Now we have everything we need to write our new command:

var cornify = function () {
    console.log(unicornAscii);
}

…and register it within the AppInit.appReady function:

CommandManager.register("Gimme some love", CORNIFY_CMD_ID, cornify);
var helpMenu = Menus.getMenu(Menus.AppMenuBar.HELP_MENU);
helpMenu.addMenuItem(CORNIFY_CMD_ID);

If you’ve added everything correctly together you should have something like this in main.js:

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window, Mustache */

define(function (require, exports, module) {
    "use strict";

    var AppInit = brackets.getModule('utils/AppInit'),
        CommandManager = brackets.getModule("command/CommandManager"),
        Menus  = brackets.getModule("command/Menus"),
        CORNIFY_CMD_ID = "artoale.cornify",
        unicornAscii = require('text!lib/unicorn.txt');

    var cornify = function () {
        console.log(unicornAscii);
    };

    AppInit.appReady(function () {
        var helpMenu = Menus.getMenu(Menus.AppMenuBar.HELP_MENU);

        CommandManager.register("Gimme some love", CORNIFY_CMD_ID, cornify);
        helpMenu.addMenuItem(CORNIFY_CMD_ID);
    });
});

Once again, reload Brackets.

You should now see in the Help menu our newly created item. Select it and have a look. Now we can create endless unicorns!

Make it glitter

Things are getting interesting now. We have a nice unicorn displaying in our dev console, but what about within the main IDE? We don’t want to switch to the dev tools each time we need some unicorn happiness.

Fortunately, there’s a nice piece of JavScript (clearly one of the best pieces of code ever written) that can help us: Cornify

Create a new file inside your “lib” subdirectory and call it “cornify.js”. I’ve prepared a slightly modified version which includes support for loading via RequireJS (really, it’s just the original code wrapped within the define call and the globals made local and assigned as a property of the exports object). The implementation isn’t really important so just copy and paste the code below:

define(function (require, exports, module) {
    var cornify_count = 0;
    var cornify_add = function () {
        cornify_count += 1;
        var cornify_url = 'http://www.cornify.com/';
        var div = document.createElement('div');
        div.style.position = 'fixed';

        var numType = 'px';
        var heightRandom = Math.random() * .75;
        var windowHeight = 768;
        var windowWidth = 1024;
        var height = 0;
        var width = 0;
        var de = document.documentElement;
        if (typeof (window.innerHeight) == 'number') {
            windowHeight = window.innerHeight;
            windowWidth = window.innerWidth;
        } else if (de && de.clientHeight) {
            windowHeight = de.clientHeight;
            windowWidth = de.clientWidth;
        } else {
            numType = '%';
            height = Math.round(height * 100) + '%';
        }

        div.onclick = cornify_add;
        div.style.zIndex = 10;
        div.style.outline = 0;

        if (cornify_count == 15) {
            div.style.top = Math.max(0, Math.round((windowHeight - 530) / 2)) + 'px';
            div.style.left = Math.round((windowWidth - 530) / 2) + 'px';
            div.style.zIndex = 1000;
        } else {
            if (numType == 'px') div.style.top = Math.round(windowHeight * heightRandom) + numType;
            else div.style.top = height;
            div.style.left = Math.round(Math.random() * 90) + '%';
        }

        var img = document.createElement('img');
        var currentTime = new Date();
        var submitTime = currentTime.getTime();
        if (cornify_count == 15) submitTime = 0;
        img.setAttribute('src', cornify_url + 'getacorn.php?r=' + submitTime + '&url=' + document.location.href);
        var ease = "all .1s linear";
        //div.style['-webkit-transition'] = ease;
        //div.style.webkitTransition = ease;
        div.style.WebkitTransition = ease;
        div.style.WebkitTransform = "rotate(1deg) scale(1.01,1.01)";
        //div.style.MozTransition = "all .1s linear";
        div.style.transition = "all .1s linear";
        div.onmouseover = function () {
            var size = 1 + Math.round(Math.random() * 10) / 100;
            var angle = Math.round(Math.random() * 20 - 10);
            var result = "rotate(" + angle + "deg) scale(" + size + "," + size + ")";
            this.style.transform = result;
            //this.style['-webkit-transform'] = result;
            //this.style.webkitTransform = result;
            this.style.WebkitTransform = result;
            //this.style.MozTransform = result;
            //alert(this + ' | ' + result);
        }
        div.onmouseout = function () {
            var size = .9 + Math.round(Math.random() * 10) / 100;
            var angle = Math.round(Math.random() * 6 - 3);
            var result = "rotate(" + angle + "deg) scale(" + size + "," + size + ")";
            this.style.transform = result;
            //this.style['-webkit-transform'] = result;
            //this.style.webkitTransform = result;
            this.style.WebkitTransform = result;
            //this.style.MozTransform = result;
        }
        var body = document.getElementsByTagName('body')[0];
        body.appendChild(div);
        div.appendChild(img);

        // Add stylesheet.
        if (cornify_count == 5) {
            var cssExisting = document.getElementById('__cornify_css');
            if (!cssExisting) {
                var head = document.getElementsByTagName("head")[0];
                var css = document.createElement('link');
                css.id = '__cornify_css';
                css.type = 'text/css';
                css.rel = 'stylesheet';
                css.href = 'http://www.cornify.com/css/cornify.css';
                css.media = 'screen';
                head.appendChild(css);
            }
            cornify_replace();
        }
    }

    var cornify_replace = function () {
        // Replace text.
        var hc = 6;
        var hs;
        var h;
        var k;
        var words = ['Happy', 'Sparkly', 'Glittery', 'Fun', 'Magical', 'Lovely', 'Cute', 'Charming', 'Amazing', 'Wonderful'];
        while (hc >= 1) {
            hs = document.getElementsByTagName('h' + hc);
            for (k = 0; k < hs.length; k++) {
                h = hs[k];
                h.innerHTML = words[Math.floor(Math.random() * words.length)] + ' ' + h.innerHTML;
            }
            hc -= 1;
        }
    }

    exports.add = cornify_add;
});

As you can deduce from the last line, we expose an add method which we’re going to use in our extension. First, we need to load this as a dependency in our main.js file (and we can replace the cornify variable since we don’t need it anymore):

var cornify = require('lib/cornify');

Notice that, since we are including an AMD module we have stripped out the text! prefix and we don’t even have to specify the .js extension. At this moment cornify corresponds to the export object inside our dependency: as in, it is an object with the exported add method. In that case we have to change another line, because the register module requires a function, not an object.

CommandManager.register("Gimme some love", CORNIFY_CMD_ID, cornify.add.bind(cornify));

In this particular case calling bind is not required as we could have simply passed cornify.add but, since we’re not technically supposed to know the internals of each and every library we use, this is a nice tip to make sure that the add function is called with the proper context. For more on context binding see Function.prototype.bind.

Your final main.js script should look like this:

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window, Mustache */

define(function (require, exports, module) {
    "use strict";

    var AppInit = brackets.getModule('utils/AppInit'),
        CommandManager = brackets.getModule("command/CommandManager"),
        Menus  = brackets.getModule("command/Menus"),
        CORNIFY_CMD_ID = "artoale.cornify";

    var cornify = require('lib/cornify');

    AppInit.appReady(function () {
        var helpMenu = Menus.getMenu(Menus.AppMenuBar.HELP_MENU);

        CommandManager.register("Gimme some love", CORNIFY_CMD_ID, cornify.add.bind(cornify));
        helpMenu.addMenuItem(CORNIFY_CMD_ID);
    });
});

I suppose at this point you already know the next steps by heart: save everything; reload Brackets; and choose your menu command again to experience pure joy.

Bonus: keyboard shortcut

Many developers love their keyboard more than their mother, and since Brackets is developed for them, adding a key binding to your menu commands is easy:

helpMenu.addMenuItem(CORNIFY_CMD_ID, "Ctrl+Alt+U");

This tells Brackets to bind our command to the specified keystroke combination (and don’t worry, Ctrl is replaced by Cmd automatically on Mac OS X).

This article was originally published at http://artoale.com/tutorial/brackets/2013/09/30/writing-brackets-extension-01/

*

*

Top