Language Handlers

All language tooling in Cloud9 is implemented using language handlers. Language handlers are plugin components that run in a web worker. Plugin developers can write language handlers directly or they can use outline definitions in the Cloud9 Bundle or the Jsonalyzer framework as an abstraction for building certain kinds of language plugins.

Overview

Language Plugins

Cloud9 language plugin packages generally have a structure like this:

└─ mylang.language
    ├─ worker
    |    └─ mylang_handler.js
    ├─ mylang.js
    ├─ package.json
    └─ README.md
    └─ LICENSE

Below is the contents mylang.js, which is the main file of this package. The mylang.js plugin is a regular Cloud9 plugin, and whose job it its to load the language handler called mylang_handler:

define(function(require, exports, module) {
    main.consumes = ["Plugin", "language"];
    main.provides = ["mylang"];
    return main;
          
    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var plugin = new Plugin("mylang.org", main.consumes);
        var language = imports.language;

        function load() {
            language.registerLanguageHandler(
                "plugins/mylang.language/worker/mylang_handler",
                function(err, handler) {
                    if (err) return err;
                    // send/receive events using handler.emit/on
                },
                plugin
            );
        }
        plugin.on("load", load);
        plugin.on("unload", function() {
            // do when plugin is unloaded
        });
        register(null, { mylang: plugin });
    }
});

Essentially all this plugin does is call language.registerLanguageHandler() to load a handler plugins/mylang.language/worker/mylang_handler.js. That file is where the real magic happens. We'll dive further into language handlers next.

Language Handlers

Language handlers implement the base_handler abstract class. The interface is best illustrated with an example. Below is a language handler that subclasses base_handler:

define(function(require, exports, module) {
    var baseHandler = require("plugins/c9.ide.language/base_handler");
    var handler = module.exports = Object.create(baseHandler);
          
    handler.handlesLanguage = function(language) {
        return language === "javascript" || language === "jsx";
    };
          
    handler.analyze = function(value, ast, callback) {
        if (!ast)
            return;
        callback(null, [{
            pos: { sl: 0, el: 0, sc: 0, ec: 0 },
            type: "info",
            message: "Hey there! I'm an info marker"
        }]);
    };
});

The base_handler abstract has one method that always must be implemented: base_handler.handlesLanguage(). In the language handler above, the handler uses it to tell Cloud9 that it should be used for JavaScript and JSX files. The handler also implements the optional base_handler.analyze() method, adding a new info marker at the top of all JavaScript/JSX files:

15701570

The example above provides a starting point for implementing language handlers. Cloud9 supports and recommends the use of multiple handlers and handler functions per language. The base_handler interface provides methods for doing code completion, showing an outline view, tooltips, etc. that are best organized into separate handlers.

👍

Divide language tooling into as many handlers modules as you like

Each language can have many language handlers that implement some subset of the supported handler methods. For example, for Python, you may have a python_linter.js, a python_completer.js and a python_outline.js, each offering some kind of language support. They all must at least have an implementation of base_handler.handlesLanguage().

Cloud9 also allows multiple handlers to implement the same base_handler method for a language. For example, there may be one implementation of base_handler.analyze() that uses ESLint for error markers in JavaScript, and another implementation that adds a silly "Hello there! I'm an info marker" marker at the top of each JavaScript file.

Below is an overview of all abstract methods of base_handler and their applications. We use RFC 2119 keywords must, should, and may to indicate which methods are required, recommended, and optional. For a full reference of all methods see the API Reference.

📘

Base handler source code

In addition to the API reference and this guide, you can have a look at the base_handler.js source code on github to browse through the language handler base class.

Declaring What a Handler is Used For

All language handlers must implement base_handler.handlesLanguage():

handler.handlesLanguage = function(language) {
    return language === "javascript" || language === "jsx";
};

Language handlers should specify a regular expression that matches identifier characters in that language using base_handler.getIdentifierRegex():

handler.getIdentifierRegex = function() {
    return /[A-Za-z0-9$_]/;
};

Language handlers may specify if they are to be used for normal editors and/or the immediate view using base_handler.handlesEditor(). HANDLES_EDITOR indicates the handler can only be used in a normal editor; HANDLES_IMMEDIATE indicates it can be used only in an immediate view, and HANDLES_EDITOR_AND_IMMEDIATE indicates both.

handler.handlesEditor = function() {
    return this.HANDLES_EDITOR;
};

Language handlers may specify the maximum file size they support:

handler.getMaxFileSizeSupported = function() {
    return 10 * 1000 * 80;
};

Implementing Static Analysis

👍

Use existing language tools when possible

Writing a full static analyzer or code completer from scratch is not easy and is only something only few people ever do. It's also a lot of work. Luckily, for many languages there are open-source language tools available that can be reused. See also Using Existing Language Tools for examples of using existing language tools.

Language handlers that perform static analysis must implement base_handler.analyze().

handler.analyze = function(value, ast, options, callback) {
    callback(null, [
        {
            pos: { sl: 1, el: 1, sc: 4, ec: 5 },
            type: "warning",
            message: "Assigning to undeclared variable."
        }
    ]
};

Implementing a Parser

Language handlers that use a parser should implement base_handler.parse():

handler.parse = function(docValue, options, callback) {
    var ast = ...;
    callback(null, ast);
};

Cloud9 does not directly use the abstract syntax tree result from a parser, but will pass it to other language handler methods when they are called. Language handlers with a parser should also implement base_handler.analyze() to display syntax errors. Languages can have multiple handlers implementing analyze(), e.g. one that returns semantic errors and one that returns syntax errors.

Language handlers that use parsing should implement base_handler.findNode() and should implement base_handler.getPos():

handler.findNode = function(ast, pos, callback) {
    var node = ...traverse the ast, searching for pos.row and pos.column...;
    callback(null, node);
};
handler.getPos = function(node, callback) {
    var pos = ...;
    callback(null, { sl: pos.sl, el: pos.el, sc: pos.sc, ec: pos.ec });
};

The findNode() method is used to pass the current AST node under the cursor to other language handlers. Other handler methods, such as base_handler.complete() can use options.node to get the current AST node when parse() and findNode() are implemented.

📘

Parsing methods are optional

Both parse() and findNode() are optional and are only intended for those languages with a parser that runs in the browser. For many languages these methods do not need to be implemented.

Implementing Code Completion

Language handlers that provide code completion must implement base_handler.complete():

handler.complete = function(doc, ast, pos, options, callback) {
    var line = doc.getLine(pos.row);
    var identifier = options.identifierPrefix;
    ...
    callback(null, [
        {
            name: "foo()",
            replaceText: "foo(^^)",
            icon: "method",
            meta: "FooClass",
            doc: "The foo() method",
            docHead: "FooClass.foo",
            priority: 1
        }
    ]);
};

Language handlers that provide code completion should implement base_handler.getCompletionRegex().

handler.getCompletionRegex = function() {
    return /^\.$/;
}

If a completion regex is specified, continuous completion is enabled for that language. This means that whenever a user types a character, completion is attempted with a short delay. If a character sequence matching the completion regex is typed, completion is performed immediately.

👍

Further reading on code completion

See Using Existing Language Tools for concrete code completion examples, and Customizing Code Completers for advanced ways of customizing a code completer.

Implementing an Outline View

23922392

Language handlers that provide an outline view must implement base_handler.outline():

handler.outline = function(doc, ast, callback) {
    callback(null, {
        result: {
            items: [
                 icon: 'method',
                 name: "fooMethod",
                 pos: this.getPos(),
                 displayPos: { sl: 15, sc: 20 },
                 items: [ ...items nested under this method... ],
                 isUnordered: true
           ]
    });
};

Generally, to create a full outline view for a language, a parser is needed. A parser can create an abstract syntax tree, from which an outline view can be constructed. If a language does not have a parser, it's possible construct a (basic) outline view can be constructed using simple string operations and regular expressions instead. See also Jsonalyzer Handlers for using the Jsonalyzer to add a simple outline using regular expressions, or use the outline bundle to do so.

Implementing Tooltips

Language handlers that provide tooltips must implement base_handler.tooltip():

handler.tooltip = function(doc, ast, pos, options, callback) {
    callback(null, {
        hint: "Hello this is a <b>tooltip</b>",
        displayPos: { sl: 0, el: 0, sc: 0, ec: 0 },
        pos: { sl: 0, el: 0, sc: 0, ec: 0 },
    });
};

Tooltips can use HTML for formatting or Cloud9's JSON format for describing tooltips for function/method signatures. See the API reference for further details.

Language handlers that provide tooltips should implement base_handler.getTooltipRegex() to speed up display of tooltips.

👍

Tooltip guessing

For languages that don't implement full tooltip support, Cloud9 can also guess the tooltips from the code completer. See Customizing Code Completers.

Implementing Jump to Definition

Language handlers that provide jump to definition must implement base_handler.jumpToDefinition().

handler.jumpToDefinition = function(doc, ast, pos, options, callback) {
    var definition = ...;
    callback(null, {
        row: definition.row,
        column: definition.column,
        path: definition.path
    });
};

Implementing Highlight Occurrences

Language handlers that provide occurrence highlighting must implement base_handler.highlightOccurrences().

handler.highlightOccurrences = function(doc, ast, pos, options, callback) {
    ...
    callback(null, {
        markers: [
            {
                pos: { sl: 5, el: 5, sc: 4, ec: 7 },
                type: 'occurrence_main'
            },
            {
                pos: { sl: 6, el: 6, sc: 4, ec: 7 },
                type: 'occurrence_other'
            },
        ],
        refactorings: false
    });
};

Implementing Code Formatting

Language handlers that provide code formatting must implement base_handler.codeFormat().

handler.codeFormat = function(doc, ast, pos, options, callback) {
    ...
    callback(null, doc.getValue().replace(/\t/, "    "));
};

Implementing Quick Fixes and Quick Suggestions

Language handlers that provide quick fixes or quick suggestions must implement base_handler.getQuickFixes().

Quick fixes allow users to click on error markers (or use a hotkey) to quickly fix common problems. At the time of writing, quick fixes are not fully supported as there is no UI for quick fixes yet. Cloud9 does support a keyboard shortcut, however, that can be used for quick fixes or quick suggestions.

Quick suggestions allow users to make quick changes to the code when there may not be actually any problem. One example of quick fixes in Cloud9 is used for the Salesforce integration, where a quick suggestion when typing an attribute name that cannot be found:

785785

Implementing a quick suggestion requires implementing both base_handler.tooltip() and base_handler.getQuickFixes(), and the use of worker_util.getQuickFixKey:

handler.tooltip =  function(doc, ast, pos, options, callback) {
    if (!isUnknownAttribute(doc, pos)) // cursor is not at something we can quick suggest for
        return callback();
        
    var key = workerUtil.getQuickfixKey();
    
    callback(null, {
        hint: "Press " + key + " to create " + expression + "()",
        pos: { sl: pos.row, el: pos.row, sc: pos.column, ec: pos.column },
        displayPos: { row: pos.row, column: pos.column }
    });
};

handler.getQuickfixes = function(doc, ast, pos, options, callback) {
    if (!isUnknownAttribute(doc, pos))) // cursor is not at something we can quick suggest for
        return callback();
    
    ...
    return callback(null, {
        action: "insert",
        path: fixPath, // path of target file
        start: fixPos, // position to apply fix
        end: fixPos,
        lines: [ // lines to insert at fixPos
            name + ": function(component, event, helper) {}"
        ]
    });
};

Implementing Rename Refactoring

Language handlers that implement rename refactoring must implement the following methods:

and may implement:

Please refer to the API reference for additional details, and see c9.ide.language.javascript/scope_analyzer.js for a concrete example.

Listening to General Events

Language handlers may implement base_handler.init() to perform any initialization as they are loaded:

handler.init = function(callback) {
    // ...
    callback();
};

Other event handlers language handlers may implement are:

Talking to the Outside World

Language workers live in their own world, in a web worker. They cannot directly talk to other plugins. There are a number of ways to still communicate with other plugins, change or invoke something on the workspace, or reach out to the web.

Every language plugin developer should have a look at worker_util. It provides several methods for talking to the outside world, including:

Language workers can also communicate with plugins that run in the UI by emitting events. In the UI plugin where a handler is registered, events can be sent and received using the handler object acquired at registration:

language.registerLanguageHandler("plugins/your.plugin/handler/mylang_handler", function(err, handler) {
    if (err) return console.error(err);
    handler.emit("getHello", {});
    handler.on("getHelloResult", function(e) {
        console.log(e);
    });
});

In language handlers, base_handler.getEmitter() allows sending/receiving events:

handler.init = function() {
    var emitter = handler.getEmitter();
    emitter.on("getHello", function() {
        emitter.emit("getHelloResult", "hello");
    });
    callback();
};

Reference Implementations

For full reference implementations of the language handler plugins, see Full Example Language Handlers.