Writing an extension for Cloud9 Javascript IDE
We've released Cloud9 IDE last weekend on JSConf.eu and made it available on GitHub. In 4 days the project has gained 340 watchers and almost 50 forks! The tagline for the Cloud9 is For Javascripters by Javascripters. This tagline is kinda recursive. It means you can hack on the IDE and make it better. We've set up Cloud9 especially with this in mind; every piece of functionality in Cloud9 is an extension. We use the great requireJS library to load all the extensions at the start of the application. For the UI we use ajax.org platform (apf) which allows us to easily modularize the user interface of Cloud9. Let's dive into writing extensions for Cloud9.
An extension starts its life as a requireJS module. I will outline the basic syntax for requireJS. For more in depth information check out the excellent requireJS documentation here. An extension depends on several other extensions and some core modules. Let's create an extension that formats JSON based on the selection of the editor. In this case our extension will depend on core/ide, core/ext, core/util and the editors manager extension ext/editors/editors. Lets call this extension formatjson and put it in the ext folder.
require.def("ext/formatjson/formatjson",
["core/ide",
"core/ext",
"core/util",
"ext/editors/editors",
"text!ext/formatjson/formatjson.xml"],
function(ide, ext, util, editors, markup) {
return ext.register("ext/formatjson/formatjson", {
//Object definition
});
}
);The arguments ide, ext and editors are filled with object references of these extensions. The 4th dependency of the formatjson extension is an xml file which is loaded as text. The 'text!' syntax tells requireJS to not parse the file as javascript but just return the contents of the file as text in the markup variable. The callback of the require.def method is called when all dependencies are loaded. At that time we register an extension on the extension manager. Let's take a look at the anatomy of the extension itself.
{
name : "JSON Formatter",
dev : "Your Name Here",
alone : true,
type : ext.GENERAL,
markup : markup,
hook : function(){},
init : function(){},
enable : function(){},
disable : function(){},
destroy : function(){}
}The following table describes each property and method:
Properties
| Property | Required | Description |
|---|---|---|
| name | required | The name of the extension is displayed in the extension manager. |
| dev | optional | The name of the developer is displayed in the extension manager. This is mostly for honor. |
| alone | optional | Boolean specifying whether this extension can live alone or only as a child of another extension. |
| type | optional | The extension type. Currently ext.GENERAL and ext.EDITOR are supported. This property will most likely be deprecated in a future version. |
| markup | optional | String containing the markup with a the UI definition for this extension. |
| visible | optional | Boolean specifying whether this extension is visible at load time. This property is only used for Panel Extensions. |
Methods
| Property | Required | Description |
|---|---|---|
| hook | optional | This function is called when the extension registers. It allows you to delay initialization of the extension. For instance you could add a menu item that triggers the initialization of the extension. At init the markup for this element is parsed and the init function is called. If you don't define a hook function the extension is initialized immediately when it registers. When you do specify a hook function you are responsible for initializing the extension yourself. An extension is initialized by calling ext.initExtension(_self);, where _self is a reference to the extension. Panel Extension For panels usually the hook function only has a single statement: panels.register(this); |
| init | required | This function is called during the initialization of the extension, after the markup is parsed. All elements in the accompanying markup of the extension are available and can be reparented to the right place. This function is also called when enabling the extension in the extension manager. Editor Extension For editor extension the first argument of the init function is the tab page element that the editor extension can fill with UI. Panel Extension For panels it is required to set this.panel to an element that will operate as a panel in the Cloud9 UI (usually a window element). |
| enable | required | This function is called when the extension is enabled. This should not be mistaken with enabling/disabling extension in the extension manager, which calls destroy and init. This function is called for instance when a panel extension is shown via the Windows Menu. |
| disable | required | This function is called when the extension is disabled. This should not be mistaken with enabling/disabling extension in the extension manager, which calls destroy and init. This function is called for instance when a panel extension is hidden via the Windows Menu. |
| destroy | required | This function is called during deinitialization of the extension. It should do proper cleanup of all created UI elements, event handlers and other state it brought into Cloud9. This function is called when disabling the extension in the extension manager. |
Implementing Format JSON
So, now that we covered the basics, lets implement the format json extension. We'll start by filling in the properties and methods that we need. I'll add a nodes array that contains all the nodes that I'll add for the UI. We'll use a hook function to create a menu item that initializes the extension and shows the format window which will allow a user to set the indentation.
{
name : "JSON Formatter",
dev : "Ajax.org",
alone : true,
type : ext.GENERAL,
markup : markup,
nodes : [],
hook : function(){
var _self = this;
this.nodes.push(
mnuEdit.appendChild(new apf.item({
caption : "Format JSON",
onclick : function(){
ext.initExtension(_self);
_self.winFormat.show();
}
}))
);
},
init : function(amlNode){
this.winFormat = winFormat;
},
enable : function(){
this.nodes.each(function(item){
item.enable();
});
},
disable : function(){
this.nodes.each(function(item){
item.disable();
});
},
destroy : function(){
this.nodes.each(function(item){
item.destroy(true, true);
});
this.nodes = [];
this.winFormat.destroy(true, true);
}
}In the hook function a menu item is created and appended to mnuEdit. mnuEdit is a global reference to the edit menu. Currently names of UI elements are put in the global namespace (this will probably change in a future version). The following is a list of available UI elements in Cloud9. The list also specifies which extension is adding the elements.
| Name | Extension | Purpose |
|---|---|---|
| mnuFile | The file menu in the top menu bar | |
| mnuEdit | The edit menu in the top menu bar | |
| mnuView | The view menu in the top menu bar | |
| mnuEditors | The editors menu in the view menu | |
| mnuModes | The layout menu in the Windows menu | |
| mnuPanels | ext/panels/panels | The windows menu in the top menu bar |
| vbMain | The main vbox of the layout | |
| tbMain | The main toolbar | |
| barMenu | The menubar | |
| barTools | The first bar in the main toolbar | |
| sbMain | The statusbar at the bottom | |
| mnuFile | ||
| mnuFile | ||
| winDbgConsole | ext/console/console | Console Panel |
| tabConsole | ext/console/console | The tab element in the console window |
| winFilesViewer | ext/tree/tree | Tree Panel |
| trFiles | ext/tree/tree | The tree element in the tree panel |
There are many more elements created. You can either find them in the respective extension or through DOM/XPath operations. For instance, between the toolbar and the statusbar there is a hbox containing 3 vbox elements.
<a:hbox>
<a:vbox />
<a:vbox />
<a:vbox />
</a:hbox>To access these you can use an XPath selector:
vbMain.selectSingleNode("a:hbox/a:vbox[2]");This will grab the second vbox in the hbox. This vbox contains the opened files tab and the console panel. You can then append any element you want to this vbox just as we do with the menu item in our json formatter extension.
Markup
So the format json extension will display the user with a window which allows this user to set the indentation size in number of spaces. We'll create the window using the aml markup syntax. I've put this in a file called formatjson.xml and added a root element a:application around the markup that I need for this extension:
<a:application xmlns:a="http://ajax.org/2005/aml">
<!-- Your UI markup here -->
</a:application>Your UI markup can consist of HTML and AML elements. For the interface of the json formatter we'll use AML to describe a window with a spinner and two buttons.
<a:window
id = "winFormat"
title = "Format JSON"
center = "true"
modal = "false"
buttons = "close"
kbclose = "true"
width = "200">
<a:vbox>
<a:hbox padding="5" edge="10">
<a:label width="100">Indentation</a:label>
<a:spinner id="spIndent" flex="1" min="1" max="20" />
</a:hbox>
<a:divider />
<a:hbox pack="end" padding="5" edge="10 10 5 10">
<a:button class="ui-btn-green" default="2" caption="Format"
onclick = "
require('ext/formatjson/formatjson').format(spIndent.value);
"/>
<a:button onclick="winFormat.hide()">Done</a:button>
</a:hbox>
</a:vbox>
</a:window>The 'Format' button has an onclick even that calls a format function on our extension. It passes the value of the spinner into this function. We have to implement this function on the extension. Lets do this now.
Custom Functions
The format function will take one argument specifying the number of spaces to use for indenting json. It will fetch the value of the current selection and check if it is valid json. Valid json is formatted and the selection is updated. If the value is not valid json an error is presented to the user.
We need one more dependency loaded to be able to do this. We need the Range module of the ace editor, so I'll add ace/Range to the dependency list in the top and call the argument 'Range'. The code for the function will look as following (I've added comments in the code to explain each part.
{
...
format : function(indent){
//We fetch the current editor from the editors extension
var editor = editors.currentEditor;
//We get the selection object from the current editor
var sel = editor.getSelection();
//We also get a reference to the current document
var doc = editor.getDocument();
//We fetch the range object of the current selection
var range = sel.getRange();
//We use this range object to get the string value of
//the selection in the context of the document.
var value = doc.getTextRange(range);
//We try to convert the value to JSON, format it
//and convert it back to a string. If this fails the user is notified.
try{
value = JSON.stringify(JSON.parse(value), null, indent);
}
catch(e){
util.alert(
"Invalid JSON",
"The selection contains an invalid or incomplete JSON string",
"Please correct the JSON and try again");
return;
}
//We replace the range with the new value
var end = doc.replace(range, value);
//We update the selection with the new position
sel.setSelectionRange(Range.fromPoints(range.start, end));
},
...
}Our extension is now ready for use. But lets add one more thing.
Key Bindings
I would like to access this extension with a hotkey: Ctrl-Shift-J for windows and Command-Shift-J for mac. In Cloud9 keys are configurable by users. In order to facilitate this we'll have to take several steps. First I'll add a new section for this extension in the default keybinding files for windows and mac in ext/keybindings_default.
...
"ext" : {
...
"formatjson" : {
"format" : "Ctrl-Shift-J" // Or "Command-Shift-J" for the mac file
},
...
}
...Then the extension needs to let the keybindings manager know for which keys it's responsible and which UI elements display the hotkeys. Start by adding two hash tables named hotkeys and hotitems:
hotkeys : {"format":1},
hotitems : {},You now have two ways of adding a handler to the keybinding. The direct approach is to add a function to the extension with the same name as the name of the keybinding; in this case 'format'. For our json format extension we have a menu item that displays the hotkey. I would like the function that is connected to the menu item's onclick to be executed when I press the hotkey. Furthermore the menu button should light up when I use the hotkey. To do this we'll tell add the menu item to the hotitems hash as follows:
this.hotitems["format"] = [this.nodes[0]];
So now our extension can be activated in the extension manager which is under the file menu. Please watch the following video to see how this extension is created in 3 minutes.
Alternative Resources
When you need help with creating an extension please use the Google Group that's set up for Cloud9. Any issues you find can be reported on the issue tracker of GitHub. All developers working on Cloud9 right now are very active on twitter as well. Good luck with extending your Cloud9 IDE. I for one can't wait to see what you'll come up with. We're very willing to add your cool new extensions as submodules to the Cloud9 IDE or even pull them in via a pull request on GitHub.
Have Fun!







Comments 4 Comments