Integrating the Ace editor into your project

I thought I’d write a quick post documenting my experiences (and frustrations) integrating the Ace editor from Cloud9 into my Google Chrome extension.

TL; DR

View the source for my nsoa-console project on GitHub to see how I implemented custom keywords, custom snippets, and custom auto-complete.

nsoa-console screenshot

I’m going to break this post into multiple parts to make it easier to consume (and in case you only care about implementing certain Ace editor features into your project).

Custom mode

Ace ships with a relatively large list of standard modes (languages and syntaxes) which is great when you’re working with a standard language. In my case, I was working with a custom syntax used at my place of employment that roughly looks like XML (it has some SQL-like syntax as well, which is why I couldn’t just use XML by itself).

In order to implement things like code folding, comments, syntax highlighting, etc., I needed to write my own custom mode file that could be used in the Ace editor. Rather than start from scratch, I cloned the Ace source and modified the mode-xml.js file (Note: the users of my extension will only ever need this one mode, so overwriting the existing XML mode worked well).

Coming from Sublime Text, where creating syntaxes is as simple as defining some regexp in a YAML or JSON file, creating Ace syntax rules was relatively convoluted. The documentation I was able to find was sparse, so I did my best at reading through the existing source code and built out some simple highlight rules.

 1 nsoa_fields: [{
 2     token: 'keyword.xml',
 3     regex: 'OA_(FIELDS(|_(SORT|GROUP)_BY|_INITIAL_ONLY)|CUSTOM(_FIELDS(|_INITIAL_ONLY)))(?=\\s)',
 4 }, {
 5     token: 'keyword.xml',
 6     regex: 'NS_(FIELDS|CUSTOM_FIELDS(|_FROM_SO_INVOICE_(HEADER|LINE_ITEM)))(?=\\s)'
 7 }],
 8 
 9 nsoa_lookup: [{
10     token:
11     [
12         'variable.parameter.xml',
13         'text.xml',
14         'variable.parameter.xml',
15         'text.xml',
16         'variable.parameter.xml',
17         'text.xml',
18         'variable.parameter.xml',
19         'text.xml'
20     ],
21     regex: '(lookup=)(\\w+)(:lookup_table=)(\\w+)(:lookup_by=)(\\w+)(:lookup_return=)(\\w+)'
22 }],

Once I got the syntax definitions defined, the only other piece that needed to be customized was block comments. In my syntax, comments follow a MySQL format using a hash or pound symbol, rather than the more standard markup comment format of <!-- // -->. To do this, a simple code addition at the end of the mode did the trick.

1 // this.blockComment = {start: "<!--", end: "-->"};
2 this.blockComment = {start: "# ", end: ""};

Custom snippets

For implementing code snippets, I ran into a lot of issues. I’m going to chalk this up to lack of knowledge on my part, but I’m also going to say again that the documentation around this piece seemed very sparse - I basically lived on Stack Overflow for a few hours as I researched this.

In the end, I found a few public Gists that implemented custom snippet managers, and I used pieces of those to formulate my snippet support.

The first part of this was to create the custom snippet manager instance which would load and process my snippets at the time of loading the editor.

 1 ace.config.loadModule('ace/ext/language_tools', function () {
 2     editor.setOptions({
 3         enableBasicAutocompletion: true,
 4         enableSnippets: true
 5     });
 6 
 7     var snippetManager = ace.require("ace/snippets").snippetManager;
 8     var config = ace.require("ace/config");
 9 
10     ace.config.loadModule("ace/snippets/xml", function(m) {
11         if (m) {
12             snippetManager.files.xml = m;
13             m.snippets = snippetManager.parseSnippetFile(m.snippetText);
14             var nsoa_snippets = nsoaGetSnippets();
15             nsoa_snippets.forEach(function (s) { m.snippets.push(s); });
16             snippetManager.register(m.snippets, m.scope);
17         }
18     });
19 });

Once you have this piece implemented, you just need to provide the snippet content. Now, according to the documentation (and the oft-referenced “kitchen sink demo”), you should be able to create a snippet file which has the your desired mode name, and the snippets will be recognized and parsed appropriately. For whatever reason, this would not work for me when using multiple snippets in the file (I could always get the first snippet to load, but nothing after that).

Again, after racking my brain and my keyboard for a while, I decided to provide my snippets in an already parsed format so that they could be passed immediately to the snippet manager from the previous step. The format was a simple array of snippet objects.

 1 function nsoaGetSnippets() {
 2     return [{
 3         name: "lookup",
 4         content: "lookup=${1:ns_field}:lookup_table=${2:oa_table}:lookup_by=${3:oa_field}:lookup_return=${4:oa_field}",
 5         tabTrigger: "lookup"
 6     },
 7     {
 8         name: "dropdown",
 9         content: "<${1:oa_field} ${2:ns_field}>\n    ${3:ns_value} ${4:oa_value}\n</${1}>\n",
10         tabTrigger: "dropdown"
11     }];
12 }

Custom auto-complete

The last part of my Ace editor customizations was implementing auto-complete. I wanted 2 important things from this: first - a custom trigger for showing the auto-complete list; second - the ability to add custom keywords to the auto-complete list (this last part being of most importance, since my custom syntax has a long list of custom keywords that are a pain to type each time).

To get this done, I actually had to modify both my custom mode file (again) and my editor file (from above). In my mode file, I needed to add a custom keywordMapper. This tells the editor which keywords should be recognized by the auto-complete engine in the language_tools extension. The format for this was a simple string list of keywords.

1 var keywordMapper = this.createKeywordMapper({
2     "tag" : "OA_CUSOM_FIELDS_INITIAL_ONLY|OA_CUSTOM_FIELDS|OA_FIELDS_INITIAL_ONLY|" +
3     "OA_FIELDS|OA_FIELDS_SORT_BY|OA_FIELDS_GROUP_BY|" +
4     "NS_PR_TASK_TO_OA_PR_TASK|NS_INVOICE_TO_OA_INVOICE|NS_EXPENSE_REP_TO_OA_EXPENSE_REP|" +
5     "NS_CUSTOM_FIELDS_FROM_SO_INVOICE_HEADER|NS_CUSTOM_FIELDS_FROM_SO_INVOICE_LINE_ITEM|" +
6     "NS_CUSTOM_FIELDS|NS_FIELDS"
7 }, "identifier");

Now that my keywords were defined, I simply wanted to customize the default trigger for showing the auto-complete list. This was done back in my editor file with a simple command and some regexp. In this case, since almost all of my keywords use underscores (see above), I wanted auto-complete to trigger when an underscore was typed and when a left angle bracket was typed.

1 editor.commands.on("afterExec", function(e){
2     if (e.command.name == "insertstring" && /^[\<_]$/.test(e.args)) {
3         editor.execCommand("startAutocomplete");
4     }
5 });

So in the end, this took me a fair amount of effort (mostly in doing hours of research to figure out how to do what I needed to do). My hope with this post is that is helps someone who comes across it on a web search - or for me the next time I need to implement this in another project. Happy coding!

to top

Tags

Archives