Ace code editor - devuxd/SeeCodeRun GitHub Wiki

Ace code editor

What is the Ace code editor, and where can I learn more about it?

Ace is web-based code editor that can be easily embedded in HTML pages as a JavaScript library. Out of the shelf, it offers several features such as syntax highlighting, auto-format, drag and drop, code folding and more. Currently, it is capable of doing live syntax check in JS/ CoffeeScript/ CSS/ XQuery. Also, it provides an API to extend its functionalities and add custom features. It is widely used in the industry, with the most notorious use being the code editor of the web-based IDE, Cloud9.

The following diagram provides an overview of Ace components. Please check other questions to learn how to add overlays via the Virtual Renderer, capture edit and user events with the Edit Session.

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] (http://stackoverflow.com/questions/tagged/ace-editor).

How do I embed it in my page?

  1. Add the library from CDNJS:
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js" type="text/javascript" charset="utf-8"></script>
  1. Add your section on your web page:
<div id="editor">
    function foo(items) {
        var x = "All this is syntax highlighted";
        return x;
    }
</div>
  1. and initiate the editor session:
<script>
    var editor = ace.edit("editor");
    editor.setTheme("ace/theme/monokai");
    editor.getSession().setMode("ace/mode/javascript");
</script>

How do I add decorations (e.g. breakpoints) to the gutter?

For custom items in the gutter use editor's method addGutterDecoration(Number row, String className). This example uses the built-in breakpoint logic. Capture the mouse down event in the editor, resolve the line and add or remove the breakpoint:

editor.renderer.setShowGutter(true);		
editor.on("guttermousedown", function(e){ 
	var target = e.domEvent.target; 
	
	if (target.className.indexOf("ace_gutter-cell") == -1){ 
		return;
	}
	if (!editor.isFocused()){ 
		return;
	}
	if (e.clientX > 25 + target.getBoundingClientRect().left){ 
		return; 
	}
	var row = e.getDocumentPosition().row;
	var breakpointsArray = e.editor.session.getBreakpoints();
	if(!(row in breakpointsArray)){
		e.editor.session.setBreakpoint(row);
	}else{
		e.editor.session.clearBreakpoint(row);
	}
		e.stop(); 
}); 
editor.session.on("changeBreakpoint", function(e){
// captures set and clear breakpoint events
});

This is the style used for the breakpoint:

.ace_gutter-cell.ace_breakpoint{ 
	border-radius: 20px 0px 0px 20px; 
	box-shadow: 0px 0px 1px 1px red inset; 
}

Find more info here

I want to get an event whenever the selection changes, either by mouse click or by keyboard input. Can I do that?

The mouse events and keystrokes are handled separately in Ace. First, to get the selection change event use this snippet.

editor.getSession().selection.on('changeSelection', function(e) {

});

To listen to mouse clicks.

editor.on("mousedown", function(e){
		// e.clientX and e.clientY for mouse coordinates
});

To listen to keyboard inputs. Here, the console shows the name of the keystroke.

editor.commands.on("exec", function(e) {
	console.log(e.command.name);
});

All put together. Here there is a global variable to store the last user input and check it after a selection change.

// detecting user input action
var selectionAction = "NONE";
editor.on("mousedown", function(e){
		selectionAction = "MOUSE";
		// e.clientX and e.clientY for mouse coordinates
});
		

var selectionKeyStrokes = [
	//"golineup", "gotoright" ,"golinedown","gotoleft", // if you want to catch cursor events
	"selectup", "selectright" ,"selectdown","selectleft",
];
editor.commands.on("exec", function(e) {
	if(selectionKeyStrokes.indexOf(e.command.name) > -1){
		selectionAction = "KEYBOARD";
	}		
});

editor.getSession().selection.on('changeSelection', function(e) {
	var range = editor.getSelectionRange();
	if(range){
		console.log(selectionAction);
	}
});

I want to build a visualization that overlays visual shapes on top of the code and I want it to move smoothly when the code scrolls. What's the best way to do that?

The best way to add visualizations in the editor is adding anchors to the Document object of the Edit Session. Then using the API to communicate the Virtual Renderer module via the Editor module( API’s entry class). if you want to popup only text, extend the autocomplete feature of Ace.

The following snippet shows how to add an icon that moves with the scroller and points out to the last selection.

  1. Let's define the overlay in the body of the HTML page (or where you need it).
<canvas id ="overlay" width="20" height="20"> </canvas>

In this case is a canvas that we force to be upfront with the z-index via a CSS style.

#overlay{
	position:absolute;
	z-index: 100;		
}
  1. Let's implement how that overlay is drawn. We receive the position where is going to be drawn as a parameter (position.pageX and position.pageY).
function updateOverlay(position){
	var div =document.getElementById("overlay");
	div.style.left = position.pageX + 'px';
	div.style.top = position.pageY + 'px';
	// Check the element is in the DOM and the browser supports canvas
	if(div.getContext) {
		// Initialize a 2-dimensional drawing context
		var context = div.getContext('2d');
		//Canvas commands go here
		// Create the yellow circle
		context.strokeStyle = "#000000";
		context.fillStyle = "#FFFF00";
		context.beginPath();
		context.arc(10,10,5,0,Math.PI*2,true);
		context.closePath();
		context.stroke();
		context.fill();
	}
  1. Now, we add an anchor to the document. An anchor will follow Ace's changes to the document, so we only have to worry about the events and not to calculate the position. For these snippets, let's assume the editor have been instantiated. For that, we need to activate the scrolling animations.
// adding overlays
editor.renderer.setAnimatedScroll(true);

Then, we get the document and add the anchor to a default position. The default depends on the logic you want. For this example, we are adding an overlay to the current text selection.

var aceDocument = editor.getSession().getDocument();
var anchor = aceDocument.createAnchor( 0, 0);
anchor.on("change", function(e){
	updateOverlay(editor.renderer.textToScreenCoordinates(anchor.getPosition())); 
});
updateOverlay(editor.renderer.textToScreenCoordinates(anchor.getPosition())); 

We should catch the scrolling events to avoid that the overlay stays visible when the anchor is not.

editor.getSession().on("changeScrollLeft", function(scrollLeft){
	updateOverlay(editor.renderer.textToScreenCoordinates(anchor.getPosition())); 
});

editor.getSession().on("changeScrollTop", function(scrollTop){
	updateOverlay(editor.renderer.textToScreenCoordinates(anchor.getPosition())); 
});

And for the example, catch the selection events.

editor.getSession().selection.on('changeSelection', function(e) {
	var range = editor.getSelectionRange();
	if(range){
		anchor.setPosition(range.end.row, range.end.column, false);
	}
});

Sadly, the layers are not exposed in the API. That is, there is no direct way to add custom overlays into the Virtual Renderer. I spent more time analyzing the API and code than doing it from scratch. That being said, I found inspiration in the Popup and Tooltip implementation used for Ace's autocomplete and hints logic, respectively. Information about Ace’s structure can be found here https://ace.c9.io/#nav=api.

I want to create a popup that comes up whenever the user hovers over a piece of code. What's the best way to do that?

The best way is to add it yourself, I will show you how. The API does not have the tooltips functionality. After reading the code and StackOverFlow, adding tooltips requires your own implementation. You can read Ace's Token ToolTip demo and a run it in its [Kitchen Sink] (https://ace.c9.io/build/kitchen-sink.html), check the "Show token info" to see the tooltip. The problem is that the Tooltip class has no binding to events and it is done in the Token demo, which has a lot of code not required for a normal tooltip. I simplified the tooltip logic into the a simpler case.

Now, We can actually add a simple tooltip if we know the mouse events and can identify what are we hovering over. In the following example, whenever a the mouse hovers over a word of the document, the tooltip will show that word.

  1. We need a CSS Style. I borrowed this from the previous Token Tooltip.
.seecoderun_tooltip {
	background-color: #FFF;
	background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.1));
	background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));
	border: 1px solid gray;
	border-radius: 1px;
	box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
	color: black;
	max-width: 100%;
	padding: 3px 4px;
	position: absolute;
	z-index: 999999;
	-moz-box-sizing: border-box;
	-webkit-box-sizing: border-box;
	box-sizing: border-box;
	cursor: default;
	white-space: pre;
	word-wrap: break-word;
	line-height: normal;
	font-style: normal;
	font-weight: normal;
	letter-spacing: normal;
	pointer-events: none;
}
  1. We show the tooltip depending if there is text to show. This is the function to draw the tooltip based on a pixel-based position. You can customize it and send your custom overlay by changing the inner HTML of the container. In this case, the text is being sent to the inner text of the container.
function updateTooltip(position, text){
	//example with container creation via JS
	var div = document.getElementById('tooltip_0');
	if(div === null){
		div = document.createElement('div');		
		div.setAttribute('id', 'tooltip_0'); 
		div.setAttribute('class', 'seecoderun_tooltip'); // and make sure myclass has some styles in css
		document.body.appendChild(div);
	}

	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 = "";
	}	
}
  1. The final part is to listen to mouse events. Once we gather the row and column that the pointer is, we get the range of the closet token to the pointer and obtain the text. Then we call the tooltip drawing function. It adds the editor's line height to put the tooltip below the word the pointer hovers.
//adding a tooltip		
editor.on("mousemove", function (e){
	var position = e.getDocumentPosition();
	if(position){
	//console.log("mouse " + position.row + ", " + position.column);
		var wordRange = editor.getSession().getWordRange(position.row , position.column);
		var text = editor.session.getTextRange(wordRange);
		if(text.length>0){
			var pixelPosition = editor.renderer.textToScreenCoordinates(position);
			pixelPosition.pageY += editor.renderer.lineHeight;
			updateTooltip(pixelPosition, text);
		}else{
			updateTooltip(editor.renderer.textToScreenCoordinates(position));
		}
	}
});

How do I add information to the gutter?

AKA Annotations can be added using the edit session and adding the required info to the annotation entry.

First, get the annotation array.

var annotations = aceEditor.getSession().getAnnotations();		

Second, add the following information to the entry.

annotations.push({type: "info", "row": row, "column": column, "raw": " y is called x times", "text": name + ' is called ' + count(entry.count, 'time', 'times')});

Finally, update the changes to the edit session.

aceEditor.getSession().setAnnotations(annotations);

How do I synchronize / listen when the user scrolls the document?

This is handled as an event in the editor called changeScrollTop for vertical scrolling and changeScrollLeft for horizontal scrolling. The following code is printing the current value of the vertical scrolling

editor.getSession().on('changeScrollTop', function(scrollTop) {
	//editor.getSession().getScrollTop(); // should have the same value
	console.log("SCROLL EVENT : " + JSON.stringify(scrollTop));
	// update your gutter 
	// Do not modify the scroller here. it will result in infinite recursion
});

##How do I move what can be seen by the user, AKA scroll the document?

If you want to scroll the document, you can use the following code:

editor.getSession().setScrollTop(100);

The following example is scrolling the document as the pointer hovers it.

editor.on("mousemove", function (e){
	var position = e.getDocumentPosition();
	if(position){		
		theGutterActionToMovetheScroller(position.row);
	}
});

editor.getSession().on('changeScrollTop', function(scrollTop) {
	//editor.getSession().getScrollTop(); // should have the same value
	console.log("SCROLL EVENT : " + JSON.stringify(scrollTop));
	// update your gutter 
	// Do not modify the scroller here. it will result in infinite recursion
});
	
function theGutterActionToMovetheScroller(scrollTop){
	editor.getSession().setScrollTop(scrollTop);
}

Check this wiki's kitchen sink for examples. Be careful about absolute values in the container you use if you want to synchronize them.

More information can be found here: VirtualRenderer and ScrollBar

Where is all the code used in this wiki?

The code is available in this wiki's Kitchen Sink

⚠️ **GitHub.com Fallback** ⚠️