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
- Language Handlers
- Declaring What a Handler is Used For
- Implementing Static Analysis
- Implementing a Parser
- Implementing Code Completion
- Implementing an Outline View
- Implementing Tooltips
- Implementing Jump to Definition
- Implementing Highlight Occurrences
- Implementing Code Formatting
- Implementing Quick Fixes and Quick Suggestions
- Implementing Rename Refactoring
- Listening to General Events
- Talking to the Outside World
- Reference Implementations
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:
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
, apython_completer.js
and apython_outline.js
, each offering some kind of language support. They all must at least have an implementation ofbase_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()
andfindNode()
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
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:
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:
- base_handler.onUpdate() to get notified as files are updated.
- base_handler.onDocumentOpen() to get notified as documents are opened.
- base_handler.onDocumentClose() to get notified as documents are closed.
- base_handler.onCursorMove() to get notified as the cursor is moved.
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:
- worker_util.execFile() for executing files on the workspace and getting their stdout and stderr output.
- worker_util.spawn() for spawning longer-running processes on the workspace.
- worker_util.execAnalysis() for invoking language tools such as linters on the workspace. See also Using Existing Language Tools.
- worker_util.stat() for getting basic information about files on the file system, such as their size.
- worker_util.readFile() for reading files on the file system, or optionally getting unsaved content of open files.
- worker_util.showError() for showing an error in the UI.
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.
Updated less than a minute ago