Implementing code completion in Ace Editor


Ace (Ajax.org Cloud9 Editor) is a popular code editor for web applications. It has both pros and cons. One of the great 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.



Foreword


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


At one time, when we had a question about choosing an editor, we considered three options: Ace, Monaco, CodeMirror. We already had experience with CodeMirror, and it turned out to be very inconvenient. Monaco, of course, is cool, but Ace seemed more functional at that time.


Out of the box, Ace supports snippets for a specific language, if you connect them. These are basic constructs and keywords (such as if-else, try-except, class, def, etc). This is certainly convenient, but how to tell the user about other types available in the context of script execution? The first option is documentation ( which no one reads ). But this method has several disadvantages. Among them - obsolescence, typos, constant switching between documentation and the editor. Therefore, it was decided to integrate our tips into the editor.


Recipe


, , Ace . , Angular, , ng2-ace-editor .


npm install --save ng2-ace-editor brace ace-builds

.


Editor.component.html


<ace-editor
  id="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';
//   
import 'brace/mode/python';
//  
import 'brace/snippets/python';
//  
import '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() { }
}

, ace ng2-ace-editor.


, ace editor, - brace. , brace ace. , , ace.



β€œenableSnippets” , .


import 'brace/snippets/python'

, .




, , . .


, plunker, : name, value, score, meta. , . . ,


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

callback . Editor . Session β€” . Pos β€” , , prefix β€” .


, ace/ext/language_tools.js. ,


getDocTooltip: function(item)

innerHTML .


, :


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;

}

callback: completions . data β€” , null. , , :)


, caption. Name Meta. snippet, , . , , . : β€œ{1:variable}”. 1 β€” (, 1), variable β€” -.


:


export interface CompletionModel {
  caption: string;
  description?: string;
  snippet?: string;
  meta: string;
  type: string;
  value?: string;
  parent?: string;
  docHTML?: string;
  //  .   -  ,  - 
  inputParameters?: { [name: string]: string };
}

InputParameters. , , :)



, :


export interface MetaInfoModel {
  //  
  Name: string;
  // 
  Description: string;
  //   
  Type: string;
  //   
  Children: MetaInfoModel[];
  //  ,   
  InputParameters?: { [name: string]: string };
}

, , . , , .


, . :


  1. completions: { [name: string]: CompletionModel[] } β€” : . , . .
  2. completionsTree: { [name: string]: string[] } : . .
  3. roots: string[] β€” , .

-, getCompletions , , caption. . , , . . , - WebApi, GetRoleById. GetRoleById, . :


  1. (.. WebApi.GetRoleById, GetRoleById)
  2. , .

, - ( WebApi if. ). , .


. , ( ):


  • β€” -.
  • , , + .
  • , , . , , , .

, . (, , ).


, , -. HTML , .


getDocTooltip completion. ( ) . :


If snippet is specified in the type and docHTML is not specified , then we consider this to be a simple hint (keyword, snippet, etc.) and set the template as it sets almost by default.


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

If the object has input parameters, then it is already more difficult. It is necessary to collect the input parameters, the full path, add a description and assemble the HTML.


//   
      let signature = Object.keys(item.inputParameters)
        .map(x => `${x} ${item.inputParameters[x]}`)
        .join(', ');

      if (signature) {
        signature = `(${signature})`;
      }

      const path = [];
      //     
      if (item.parent) {
        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 result will be something like this.


For clean input:




As you can see, our classes are displayed.

To access through the point:



As you can see, after the point we only see child methods for the WebApi class .

If there is no metadata, when accessing through a point



, local data is displayed.

Conclusion


We got a pretty convenient auto-completion, which can be used with the implementation without pain :)


.

Source: https://habr.com/ru/post/undefined/


All Articles