Extending Ace: Let’s make sure you are always with higher hand

Summary

Ace is a well-known JS code editor that encompasses several visual enhancements. However, its lackluster API makes developers depend heavily on other sources to get things done. At first, Ace’s learning curve will be slow and overwhelming but with this post, I will significantly reduce that burden for you.
We required several features of Ace to be customized, as we planned to provide a novel experience for web developers. To our surprise, we often found ourselves learning how Ace works not through its API but developer forums and blogs. I will use our story as the script this post follows.
Not to worry, after playing with this post’s examples you will be able to get ace hands with your solutions. As the plot develops, I will give you several hints to get several features implemented in Ace based on our experience, from customized tooltips to overlays to resolving events correctly, and what is the best to approach your implementation and save time.

Introduction

I argue that Ace has the higher hand for out of the shelf editor, but struggles when it comes to providing intuition of how its components communicate so more complex features can be implemented.  We had three teams requiring Ace to create a custom gutter, and we had three different ways of doing so. The first was doing it in a black-box approach, which created a gutter from scratch and synchronizes with Ace’s events; the second was a white-box approach, where after reading Ace’s code, they extended it to create a new gutter; and the third approach was a grey-box, which required accommodating functionalities from Ace based on our design.
Ace’s lackluster API limited our feasible approaches to the white-box, as we could not make all pieces work without significant knowledge of what they do. Other editors, such IntelliJ for Java, offers a robust API that allows complex functionalities to be implemented with ease. Now, I will present our solution so you don’t have to read the Ace’s code too.

Ace’s Lifecycle

You can Ace using by adding to your page script. Ace will the be a global object that you can repeat at any point of your  app. The only input it needs is the container where it will be rendered.

The following snippet gets the Ace’s library from a DNJS server. Also, you can
download it and keep it on your server.


<script src=
"https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js"
 type="text/javascript" charset="utf-8">
 </script>

Now, in the following snippet, we define the container where Ace is going to be rendered.

<div id="editor">
//some code</div>

Finally, in the following snippet shows how to obtain the API’s entry point after we specified the container.

 let editor = ace.edit("editor");

More details about destroying the editor in the Ace’s repository webpage.

The best way to listen and extend Ace actions is to subscribe to its events and depart from there. In the API documentation are listed a limited set of them, so normally you have to read the Editor class to understand most of the rest of them.

The following snippet shows the how to subscribe to an Ace’s event and to bind your logic to it.

  let callback = function (data){// your logic};
  let eventName = "mousemove";
  editor.on(eventName, callback);

 

Event Types

We will be working with editor, gutter and markers. These are events where are interested in and they break down into by the originator of the event : components and user. Change the value of eventName in the previous snippet to subscribe to them

Listening to component’s changes

documentchanged: If the editor content changes, this event will be triggered.

annotationschanged: if the gutter elements changed this signal will be registered. breakpoints, gutter decorations, and annotations such warnings, errors and suggestions

There are other events that can be of help, please take a look at their documentation( the forum will be more helpful).

 

Listening to user actions

mousemove: captures cursor absolute pixel position with {clientX ,clientY} and sends it as input parameter for your callback. Also, with getDocumentPosition() you get the cursor position as Ace’s {row, column} position.

guttermousemove: Works similar to the previous event, but for the gutter.

If you need to listen to taps/clicks on the same elements use “mousedown” and “guttermousedown”, respectively. There are more events for keyboard input, selections, and others.

A nice refactoring for the Ace’s events logic

It would be really helpful to have an interface to get the list of Ace’s own events without looking at the implementation. In Ace’s code, the events names are hard coded, this could part of the problem. If they use an enumerator  kind of logic, developers could rely on checking the available events per-component. Additionally, keeping track of events provided by other components make reading their blog and code a must.

How do we extend Ace?

In our teams, one of the issues we encounter was maintaining interaction limited to messaging. After reading the API documentation, the three teams ended up each proposing a different solution. I will attribute  that to the experience with JS and goals required.

Team 1 required creating a gutter that could be really flexible in terms of adding visual widgets not limited to text. They noticed that a solution from scratch will require less learning of Ace’s code. They found significant issues related to matching their gutter with Ace as they were not aware of all the events and properties required to make it work, hence, the API does not mention such considerations about Ace’ inner components.

Team 2 read Ace’s code and extended it in the form a custom gutter. In this approach, all the events are handled showing a nice coupling with the editor. Although, Ace’s implementation is being exposed and inherits its limitations to extend components.

In team 3, we were in the middle,we needed to get and provide data to the editor. I was in charge of understanding Ace and help the previous teams in using Ace. I consider that we could have a solution somewhere in the middle, a grey-box approach.

The rationale is to have a complete yet flexible interaction with Ace without exposing the code. For that, I found some extend points in Ace where we should add new functionalities and provide binding mechanisms within a simple design.

Extensible Components

Ace contains the following components to be extended: Gutter and its cells, Edit Sessions, and its markers, and Language Mode and its syntax highlighters.

To maintain our implementation loosely coupled. I avoided depending on delegating our rendering to Ace’s. It is not intended to render objects beyond the ones contained in its layers. It is better to understand where to bind your components and add your extra layer.

Using MVC in your extension design

The main concern about implementing your code is to do it without compromising the reuse or duplication of your code.

One troublesome issue about Ace’s lack of flexibility is that the code intended to be short ends up being spread all over your implementation. This is due an architectural mismatch, that is, incompatible assumptions between client and provider of software components.

In this case, clients assume that  all the logic will be handled by callbacks but they should be handled using an MVC architecture.

I consider that this will avoid several headaches in the future as all your code is in one place, reusable and easy to extend. For this case, I suggest a MVVC architecture. We are Aurelia, where this is the default architecture. Once you know the pattern, it becomes ease to use their templates.

Our implementation

To show our data in the gutter and document, we used the following logic:

We grouped all Ace’ boilerplate into a utility library that you can use to implement your view controllers.

 class AceUtils{
constructor(){
//stateless
}

subscribeToEvents(editor, tooltip, gutterDecorationClassName, dataModel){
let updateTooltip = this.updateTooltip;
let isPositionInRange = this.isPositionInRange;
let isRangeInRangeStrict = this.isRangeInRangeStrict;

editor.on("guttermousemove", function(e){
updateTooltip(tooltip, editor.renderer.textToScreenCoordinates(e.getDocumentPosition()));
let target = e.domEvent.target;
// si the element we want? for Ace cells "ace_gutter-cell", ours is
if (target.className.indexOf(gutterDecorationClassName) == -1){
return;
}
// is this during user attention?
if (!editor.isFocused()){
return;
}
// is not the folding icon to the right of the line number?
if (e.clientX > target.parentElement.getBoundingClientRect().right - 13){
return;
}
let row = e.getDocumentPosition().row;
let text = "";

if(dataModel.rows.hasOwnProperty(row)){
text = dataModel.rows[row].text;
let pixelPosition = editor.renderer.textToScreenCoordinates(e.getDocumentPosition());
pixelPosition.pageY += editor.renderer.lineHeight;
updateTooltip(tooltip, pixelPosition, text);
}
e.stop();

});

editor.on("mousemove", function (e){
let position = e.getDocumentPosition(), match;
if(position){
// todo: Aurelia style
let result = window.TRACE? window.TRACE.getExecutionTrace() : undefined;
if(!result){
return;
}

for(let key in result){
let data = result[key];

if(data.range && isPositionInRange(position, data.range)){
if(match){
if(isRangeInRangeStrict(data.range, match.range)){
match = data;
}
}else{
match = data;
}

}
}
if(match){
let pixelPosition = editor.renderer.textToScreenCoordinates(match.range.start);
pixelPosition.pageY += editor.renderer.lineHeight;
updateTooltip(tooltip, pixelPosition, match.text +", values"+ JSON.stringify(match.values));
}else{
updateTooltip(tooltip, editor.renderer.textToScreenCoordinates(position));
}
}
});

}

updateTooltip(div, position, text){

div.style.left = position.pageX + 'px';
div.style.top = position.pageY + 'px';
if(text){
div.style.display = "block";
div.innerText = text;
}else{
div.style.display = "none";
div.innerText = "";
}
}

... more code
}

Then we defined your view-models with the data structure we wanted to keep track of and how to visualize it.


class TraceViewModel {
constructor(editor, tooltip, gutterDecorationClassName){
this.editor= editor;
this.tooltip = tooltip;
this.gutterDecorationClassName = gutterDecorationClassName;
this.aceUtils = undefined;
this.traceGutterData = { maxCount : 0, rows : [] }; // contains custom data to be shown in the gutter cell text
// remove in Aurelia
this.attached();
}

setUpEditor(editor){
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
editor.renderer.setShowGutter(true);
// editor.session.setOption("useWorker", false);
}

attached(){
this.resetTraceGutterData();
let editor = this.editor;
let tooltip = this.tooltip;
let traceGutterData = this.traceGutterData;
let gutterDecorationClassName = this.gutterDecorationClassName;

this.setUpEditor(editor);
let aceUtils = new AceUtils();
aceUtils.setTraceGutterRenderer(editor, traceGutterData);

aceUtils.subscribeToEvents(editor, tooltip, gutterDecorationClassName, traceGutterData);

this.aceUtils = aceUtils;
}
//more code
}</pre>
<pre>

Then we bound them to your controller and hope for the best. We call the TraceViewModel here:


let editor = ace.edit("editor");
 let tooltip = document.getElementById('tooltip_0');

 if(tooltip === null){
 tooltip = document.createElement('div');
 tooltip.setAttribute('id', 'tooltip_0');
 tooltip.setAttribute('class', 'seecoderun_tooltip'); // and make sure myclass has some styles in css
 document.body.appendChild(tooltip);
 }
 let gutterDecorationClassName = "seecoderun_gutter_decoration";
 var traceViewModel = new TraceViewModel(editor, tooltip, gutterDecorationClassName); 

 editor.on("documentchanged", traceViewModel.onTraceChanged(trace));</pre>
<pre>

 

This it is how its looks:

Capturetrace

 

 

References

You can learn more about it here. For more details of its structure and API, check here.

For troubleshooting and lore check Ace’s Google group and the “ace-editor” tag in StackOverflow.

More information can be found here: VirtualRenderer and ScrollBar

The code of this post is available at our Kitchen Sink

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s