Home Java Implementing autocomplete code in AceEditor

Implementing autocomplete code in AceEditor

by admin

Implementing autocomplete code in AceEditor

Ace (Ajax.org Cloud9 Editor)is a popular code editor for web applications.It has both pros and cons.One of the big advantages of the library is the ability to use custom snippets and tooltips. However, this is not the most trivial task, nor is it very well documented. We actively use the editor in our products and decided to share the recipe with the community.

Preface

We use Ace to edit notification templates and to edit custom Python functions designed to be called from the business process engine, o which we wrote about earlier

When we were looking for an editor, we considered three options: Ace, Monaco and CodeMirror. We already had an experience with CodeMirror and it turned out to be very inconvenient. Of course, Monaco is cool but Ace seemed to be more functional at that moment.

Out of the box, Ace supports language-specific snippets ifyou plug them in. These are both basic constructs and keywords (such as if-else, try-except, class, def, etc). This is certainly handy, but how do we tell the user about the other types available in the context of script execution? The first option is documentation ( which nobody reads ). But this method has a number of disadvantages. Among them are obsolescence, typos, and constant switching between documentation and editor. So we decided to integrate our tooltips into the editor.

Recipe

So, first, let’s plug Ace into our application. You can do it any way you want, and since we have an Angular frontend, for convenience, let’s install ng2-ace-editor and all of the necessary dependencies.

npm install --save ng2-ace-editorbraceace-builds

And add it to the template.

Editor.component.html

<ace-editorid="editor"#scriptEditor[options]="options"[autoUpdateContent]="true"[mode]="'python'"[theme]="'github'"[(text)]="script"[style.height.px]="600"> </ace-editor>

editor.component.ts

import { Component }from '@angular/core';import * as ace from 'brace';// for lighting syntaximport 'brace/mode/python';// for hintsimport 'brace/snippets/python';// color themeimport 'brace/theme/github';import 'brace/ext/language_tools';@Component({selector: 'app-editor', templateUrl: './editor.component.html', styleUrls: ['./editor.component.css']})export class EditorComponent {script = 'import sys\n\nprint("test")';options = {enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true};constructor() { }}

We will not go into detail about each parameter, you can read about them in the documentation ace and ng2-ace-editor

You may be surprised here, because we are talking about ace editor, and some brace That’s right, brace is a browser adaptation of ace. As it says in the readme, you need it to integrate into the browser, so you don’t have to put the same ace on the server.

Hints

"enableSnippets" enable snippets for the selected language if the corresponding module is loaded.

import 'brace/snippets/python'

Let’s check what works.

Implementing autocomplete code in AceEditor

Great, keywords, basic snippets are displayed. Local datatoo.

There is virtually no mention of the substitution data model in the documentation, except for the example at plunker , which uses four fields : name, value, score, meta It’s not quite clear what is what. And the example doesn’t work. But it is clear that the completer itself should contain method

getCompletions: function(editor, session, pos, prefix, callback)

where in callback you need to pass a list of possible substitutions. Editor is the instance of the entire editor. Session – Current session. Pos – apparently the position where the completer call was triggered and prefix – characters entered.

Let’s open the place where completers are registered ace/ext/language_tools.js And we see that the kolleter can have another method

getDocTooltip: function(item)

within which the value for the field innerHTML to display information about the object as a nice tooltip.

As a result, the completer interface :

export interface ICompleter {getCompletions(editor: ace.Editor, session: ace.IEditSession, pos: ace.Position, prefix: string, callback: (data: any | null, completions: CompletionModel[]) => void): void;getTooltip(item: CompletionModel): void;}

About callback : what is completions is clear. But what is data – not quite, because it passes everywhere null So let it be, apparently we won’t need it 🙂

During the debug it becomes clear that the engine searches the caption field. And the fields displayed in the list are Name and Meta The substitution can have a value in the snippetfield, then the substitution will work exactly as a snippet, and not just as text. By experience, we find that a snippet may contain variables that can be replaced. Their syntax is : "{1:variable}" Where 1 – is the ordinal index of the substitution (yes, counting starts with 1), and variable – is the default substitution name.

The final model we have is something like this :

export interface CompletionModel {caption: string;description?: string;snippet?: string;meta: string;type: string;value?: string;parent?: string;docHTML?: string;// Input parameters. Where is the key - name parameters, value - typeInputParameters?: { [name: string]: string };}

To output a nice tooltip, let’s add a field to the model InputParameters This is needed to output these parameters, like in a real code editor 🙂

Metadata model

We receive data from the server, roughly in this form :

export interface MetaInfoModel {// entity nameName: string;// descriptionDescription: string;//returned value typeType: string;// list of nested elementsChildren: MetaInfoModel[];// input parameters, if this is a methodInputParameters? { [name: string]: string };}

From this model, it follows that we can have methods that are called with the passing of parameters or properties. Properties can be finite, which can be called separately, or they can be a container for methods and other properties.

To use this tree more effectively, we will need to smap it into a flatter structure. Let us decompose it into the following components :

  1. completions: { [name: string]: CompletionModel[] } mapping – name : substitution list. The substitution list is needed for duplicates so that they don’t overwrite each other. We will filter by parent when retrieving a value.
  2. completionsTree: { [name: string]: string[] } mapping parent : children. Plotted tree for easy retrieval.
  3. roots: string[] – is a list of root nodes which will be given on new input.

By default, method getCompletions spits out everything it can, and the engine filters by caption. But it filters among all registered completers. So if you just add the completer to the main ones, there will be a problem. Showing cues will show ALL possible choices at any given time. For example, we have a container class WebApi and it has a method GetRoleById Then you can write a call to the method GetRoleById which is not right. There are two options here :

  1. Insert the full path (i.e. WebApi.GetRoleById , instead of GetRoleById )
  2. Do not show nested nodes until it reaches them.

Also, it is necessary to control the default hints in our compiler (so that when a dot is used to WebApi cannot be added from the tooltips if ). And determine what to show and at what point in time.

The algorithm is about the following. Determine if the input is new (no dot conversion):

  • If yes – shows the top-level containers and default hints.
  • If no , we look for the parents and use them to determine what to show next + we show text hints.
  • In the same way, if no then we show already entered values. This is necessary to show not only metadata, but also user variables if there are already calls to the entity.

To determine the parent, we need to get the current row and column. Then find a point on the left and from it to the left search for a word up to the separator (space, dot, bracket).

For a nice tooltip we will need not only the name of the method, but also the input arguments, which come as key-value. The engine expects HTML markup, so the room for imagination is huge.

Into the method itself getDocTooltip receives a specific element completion We have the input data (if any) and other settings written in it. The algorithm will be roughly as follows :

If the type specifies snippet and is not specified docHTML , then consider that it is a simple hint (keyword, snippet, etc.) and set the template as it is set almost by default.

item.docHTML = ['<b> ', item.caption, '</b> ', '<hr> </hr> ', item.snippet].join('');

If the object has input parameters, it’s more complicated. You have to collect input parameters, full path, add description and collect HTML.

// collect input parameterslet signature = Object.keys(item.inputParameters).map(x => `${x} ${item.inputParameters[x]}`).join(', ');if (signature) {signature = `(${signature})`;}const path = [];// Collecting the path to the currentif (item.parent) method {let parentId = item.parent;while (parentId) {path.push(parentId);parentId = this.completions[parentId][0].parent;}}const displayName =[...path.reverse(), item.caption].join('.') + signature;let type = item.type;if (item.meta === 'class') {type = 'class';}const description = item.description || '';let html = `<b> ${type} ${displayName}</b> <hr> `;if (description) {html += `<p style="max-width: 400px; white-space: normal;"> ${description}</p> `;}item.docHTML = html;

The end result is something like this.

For clean input :

Implementing autocomplete code in AceEditor
As you can see, our classes are displayed.
To address through a point :
Implementing autocomplete code in AceEditor
As you can see, after the dot we have only child methods for the class WebApi
If there is no metadata, when accessed with a point
Implementing autocomplete code in AceEditor
local data is displayed.

Conclusion

We have a pretty handy autocomplement, which can be used for implementations without any agony 🙂

To see the result you can here

You may also like