Create a Package

Packages are the building blocks of cloud9. Every bit of functionality of Cloud9 is implemented in a package. A package consists of one or more plugins. All plugins are first class citizens and therefore have access to all the APIs that the core Cloud9 plugins have access to. This means that you are able to extend, implement or re-implement any feature of Cloud9.

A package can contain several resources to implement your feature(s). The default package template consists of:

c9.ide.default
    images/
    snippets/
    modes/
    package.json
    plugin.html
    plugin.js
    README.md

Not every package will include all these files and directories. A package must at least contain a package.json and the implementation of a plugin in a .js file.

We have a tutorial on creating your first package.

We also have a guide on how to develop a package. That guide explains how to easily get started creating a new package and how to use Cloud9 to easily develop plugins.

package.json

Cloud9 packages contain a package.json in their top level directory similar to [npm packages](http://en.wikipedia.org/wiki/Npm_\(software\). This file contains metadata about the package such as it's version, which plugins are available and what config options the plugin(s) should be loaded with.

In addition to the regular npm package.json fields, Cloud9 package.json files have their own additions:

pluginsan object with the path relative to the package root of the plugins to load as the key, and an object containing 0 or more properties that are passed as the configuration object to the plugin. (i.e. { plugins: { "example": { "color: "red" } })
categoriesan array of all the categories this plugin can be found in (max 5)
repositorySimilar to the repository field in npm packages. For Cloud9 packages this field is required.

The Plugin

If you want to extend or change Cloud9's behavior you'll add one or more plugins to your package. Each plugin should have a single purpose and can depend on any other plugin that is available. Your plugins can also depend on each other, though circular dependencies are not allowed. Each plugin you create should be specified in the package.json.

Dependencies

Plugins provide an interfaces to the Cloud9 plugin ecosystem. These interfaces can be consumed by other plugins. There are many interfaces are available out of the box, provided by Cloud9's core plugins. Some of the core interfaces that cloud9 provides are Plugin, fs, proc and net. There are many others and I refer to the documentation for them all. In the examples below we'll use commands, ui, menus, preferences and settings.

Note that all Cloud9 plugins are client side plugins. One of the consequences of this is that we use AMD style loading of the modules (using 'define('). Cloud9 uses require.js under the hood.

The next example shows the signature of a plugin that depends on Plugin and fs:

define(function(require, exports, module) {
    main.consumes = ["Plugin", "fs"];
    main.provides = ["myplugin"];
    return main;

    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var fs = imports.fs;
        
        var plugin = new Plugin("Your name", main.consumes);
        
        register(null, {
            "myplugin": plugin
        });
    }
});

In the example above the options argument to the main function will contain the properties that are specified in the package.json. In addition it will contain the staticPrefix property and the apikey property. The staticPrefix is an absolute url pointing to the root of the package folder, which can be used to retrieve resources from that directory. The apikey property is used to store persistent data in the database and is better described in the using persistent data tutorial.

Lifecycle

The lifecycle of a basic plugin has only two events. It starts with load and ends with unload. Other plugins such as Editor have many more events. Each event in the lifecycle is triggered by an event on the plugin object.

To implement the lifecycle for a plugin, add the following code after creating an instance of the plugin:

var x = 10;

function load() {
    x = 100;
}

/***** Lifecycle *****/

plugin.on("load", function() {
    load();
});
plugin.on("unload", function() {
    x = 10;
});

In this example we introduce the x variable within the main scope. It is imperative to always reset any variables in the main scope in the unload event. Your plugin can be loaded and unloaded dynamically and this clean up is essential for that.

Note that the load event is triggered immediately after loading the package, so at the start of Cloud9. Any heavy lifting, such as creating HTML elements should be deferred until the user needs them. For more information on this, see the creating your first package tutorial.

Public API

Your plugin can expose properties, methods and events to others that reference your plugin. When setting the public API you lock down your plugin from being tampered with by others. This is important for security reasons. The example below adds a property and two events and methods to the interface of a plugin:

var plugin = new Plugin("Your name", main.consumes);
var emit = plugin.getEmitter();

var showing;

function show(){ showing = true; emit("show"); }
function hide(){ showing = false; emit("hide"); }

/***** Register and define API *****/

plugin.freezePublicAPI({
    get showing(){ return showing; },
    
    _events: [
        "show",
        "hide"
    ],
    
    show: show,
    hide: hide
});

Style Sheets

Style sheets can be placed anywhere in your package. Style sheets can be written as CSS or Less, but Less is recommended. Any style sheets that is loaded will be attached to the DOM by executing the following code:

ui.insertCss(require("text!./style.css"), options.staticPrefix, plugin);

Notice how in the example ./ is used to specify a path relative to the plugin file. By supplying the plugin as the last argument the css will be automatically removed when the plugin is unloaded. This pattern is used throughout the Cloud9 APIs. The staticPrefix contains the path to the package directory and is used to load images from your package in the css. Use @{image-path} in your css to load images and other resources.

Theming

...

HTML

In the same way as css, you can insert HTML from an .html file into the DOM with a single command:

var markup = require("text!./plugin.html");
ui.insertHtml(document.body, markup, plugin);

The last argument is again for cleanup, automatically removing the added HTML elements when the plugin is unloaded.

Readme

The README.md file is optional. It will allow others to read about and understand the function of your package. We recommend that you create and maintain a README.md file for your package.

Commands

A common use of plugins is to add extra commands to the vocabulary of Cloud9. Commands consist of code to execute a name and key binding. Commands can be referenced by menu items and buttons as way to trigger the command. Commands are always found in the commands pane and they key bindings can be configured in the key bindings editor in the preferences.

The following example shows how to create a very basic command.

commands.addCommand({
    name: "oneplusone",
    exec: function(){ 
        console.log(1 + 1);
    },
}, plugin);

The command is called oneplusone and when triggered the exec function is called, printing 2 in the browser's console. Here's a short code snippet that triggers this command:

commands.exec("oneplusone");

The next example is more detailed and specifies a lot more details about the command. This specific example is used in the format json tutorial;

commands.addCommand({
    name: "formatjson",
    group: "Format",
    bindKey: { 
        mac: "Shift-Command-J", 
        win: "Ctrl-Shift-J" 
    },
    exec: function(){ 
        formatJson() 
    },
    isAvailable: function(editor) {
        if (editor && editor.ace)
            return !editor.ace.selection.isEmpty();
        return false;
    }
}, plugin);

Of special interest are the bindKeys, which are the key combos that can be used to trigger this command. Users can change these defaults in the key bindings editor in the preference panel.

The isAvailable() function determines context for the command. In this case, the formatjson command is only available when an ace editor is focussed and there is code selected.

Menus

There are two different ways to add menus to Cloud9. The application menu has a different api from the context menu, for practical reasons.

Application Menu

You are free to place extra menu items anywhere in the menu structure of Cloud9. You can use an integer to specify the position of your menu items relative to others. The next example shows how to add a menu item to the File menu.

menus.addItemByPath("File/My Menu Item", new ui.item({
    command: "mycommand"
}), 300, plugin);

Based on the result of isAvailable() of the command, this menu will either show as enabled or disabled.

🚧

Positioning using an index

Throughout Cloud9 you'll see APIs that allow you to sort your items using integers. By using an int to set the position it is possible to determine the approximate position of, for instance an item in a menu without having any knowledge about other plugins that are loaded. Usually all plugins are a-like and there are no special ranges. Simply choose a position that makes sense to you and use it.

To see what the position ints are simply load Cloud9 with ?menus=1 in the url.

Adding a divider works in a similar way:

menus.addItemByPath("File/~", new ui.divider(), 310, plugin);

Context Menu

Commands that are linked to specific parts of the interface should be triggered by a menu item in a context menu. The context menu api is slightly different and requires the Menu, MenuItem and Divider plugins in your list of consumed plugins.

This example shows a straightforward way to create a menu:

var menu = new Menu({ items: [
    new MenuItem({ command: "open", caption: "Open..." }),
    new MenuItem({ command: "save", caption: "Save" }),
    new MenuItem({ command: "saveas", caption: "Save As..." }),
]}, plugin);

menu.show(10, 10);

It's easy to manipulate menus when needed:

var item = new MenuItem({ 
    caption : "Alert" 
    icon    : "alert.png" 
    onclick : function(){
        alert("Alert!");
    }
});
menu.append(item);

item.disabled = true;
item.caption = "No alert";

Submenus are normal context menus that are referenced by a menu item:

var listMenu = new Menu({}, plugin);
var item = new MenuItem({ caption: "List...", submenu: listMenu });

Adding Configuration Settings

There are many reasons why plugins need to store persistent data across sessions. Often this can be data specific to a user, the state of the plugin and it's UI or things related to the project.

The settings plugin allows plugins to store settings in a tree-like structure, similar to other key-value stores like Redis or the windows registry. There are three root contexts in which data can be stored. The user, project and state.

User settings are loaded on each workspace that the user is logged into. Preference like the selected theme and keyboard shortcuts are stored in the user context. Settings that are specific to a project are stored in the project context. These settings are stored in the .c9/project.settings relative to the project directory. Settings like the tab distances are stored here. Anyone using that project will load these settings. Finally, the application state is stored in the state context. Settings like a panel's width or the selection state of the tree are stored here.

As a plugin developer, you choose where to store which setting. First specify the defaults:

settings.on("read", function(){
    settings.setDefaults("user/my-plugin", [
        ["someKey", "value"],
        ["otherKey", "value"]
    ]);
}, plugin);

In this case we're adding some user settings. Set and fetch the values like this:

settings.set("user/my-plugin/@someKey", 100);
settings.getNumber("user/my-plugin/@someKey");

Often you'll want to listen to changes to a key. For instance, when a user set's a setting via the preference pane. To respond to the change of a setting use the following code:

settings.on("user/my-plugin/@someKey", function(value){
    console.log("Value changed to:", value);
}, plugin);

Note that the plugin is passed as the 3rd argument to make sure this event handler is cleaned when the plugin unloads.

Preferences

When you want to enable a user to configure some of these settings, use the preferences API to add some UI elements to the preference pane.

Simply describe the widget, the place of the widget in the navigation tree and the setting it is operating on.

prefs.add({
    "Example" : {
        position: 450,
        "My Plugin" : {
            position: 100,
            "First Setting": {
                type: "checkbox",
                setting: "user/my-plugin/@first",
                position: 100
            }
        }
    }
}, plugin);

Similar to the menus, use the position index to specify how to sort the vertical position of your widget.

The next example show's how to create a dropdown:

"Second Setting": {
    type: "dropdown",
    setting: "user/my-plugin/@second",
    width: "185",
    position: 200,
    items: [
        { value: "you", caption: "You" },
        { value: "me", caption: "Me" },
        { value: "all", caption: "All" }
    ]
}

Snippets

...

Language Modes

...

Bundle External Resources

It's easy to reference external resources from within your plugins. Here are the most common ones:

Loading additional files:

var lib = require("./some_library");

Or loading files as text:

var text = require("text!./somedata.csv");

All your CSS files are parsed as LESS files. To references images in your package simply reference the @{image-path} variable:

background: url("@{image-path}/bg.png");

Lastly, for code in the plugin itself, options.staticPrefix will give you the url to the root of the package.

iframe.src = options.staticPrefix + "/index.html";

Tests

Each plugin is required to have tests. The tests for each plugin should be in a file with the same name as your plugin and have _test.js appended to it. For a file called plugin.js the test is called plugin_test.js.

Writing Tests

Under the hood, Jasmine executes your tests, so you can assume that any DSL available there is also available to your package.

Tests are executed on the client side and they are loaded the same way your plugin is loaded. This means that you can specify any plugin as a dependency for your tests. This includes your own plugin. See plugin_test.js as a template for your tests.

Running Tests

Start Cloud9 in debug mode via Tools/Developer/Start in Debug Mode. Cloud9 will open in debug mode in a new window. You can now start via the menu Tools/Developer/Run Test/plugin.js. Where plugin.js is the name of your plugin.

Publishing

To allow others use your plugin, you need to make it available on any cdn. Make sure that you have the latest version of c9 cli installed from npm. Go to the directory of your plugin and run c9 build. This command will create c9build folder with all the code of your plugin optimized for loading.
If you are using github, you can push this folder to gh-pages branch of your repository, but any other cdn will work too.

Test that your plugin works by copying the url of c9build/package.<plugin.name>.js file, and loading it using your init script:

services.pluginManager.loadPackage([
        "https://cdn.url/to/package.<plugin.name>.js", 
])

Create a post on https://community.c9.io to let other users find about your plugin.

See also https://community.c9.io/t/distributing-and-installing-custom-cloud9-plugins/15168