Editor Lite

Editor Lite provides the editing experience at GitLab. This thin wrapper around the Monaco editor provides necessary helpers and abstractions, and extends Monaco using extensions. Multiple GitLab features use it, including:

How to use Editor Lite

Editor Lite is framework-agnostic and can be used in any application, including both Rails and Vue. To help with integration, we have the dedicated <editor-lite> Vue component, but the integration of Editor Lite is generally straightforward:

  1. Import Editor Lite:

    import EditorLite from '~/editor/editor_lite';
    
  2. Initialize global editor for the view:

    const editor = new EditorLite({
      // Editor Options.
      // The list of all accepted options can be found at
      // https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.editoroption.html
    });
    
  3. Create an editor’s instance:

    editor.createInstance({
      // Editor Lite configuration options.
    })
    

An instance of Editor Lite accepts the following configuration options:

Option Required? Description
el true HTML Node: The element on which to render the editor.
blobPath false String: The name of a file to render in the editor, used to identify the correct syntax highlighter to use with that file, or another file type. Can accept wildcards like *.js when the actual filename isn’t known or doesn’t play any role.
blobContent false String: The initial content to render in the editor.
extensions false Array: Extensions to use in this instance.
blobGlobalId false String: An auto-generated property.
Note: This property may go away in the future. Do not pass blobGlobalId unless you know what you’re doing.
Editor Options false Object(s): Any property outside of the list above is treated as an Editor Option for this particular instance. Use this field to override global Editor Options on the instance level. A full index of Editor Options is available.

API

The editor uses the same public API as provided by Monaco editor with additional functions on the instance level:

Function Arguments Description
updateModelLanguage path: String Updates the instance’s syntax highlighting to follow the extension of the passed path. Available only on the instance level.
use Array of objects Array of extensions to apply to the instance. Accepts only the array of objects. You must fetch the extensions’ ES6 modules must be fetched and resolved in your views or components before they are passed to use. This property is available on instance (applies extension to this particular instance) and global editor (applies the same extension to all instances) levels.
Monaco Editor options See documentation Default Monaco editor options

Tips

  1. Editor’s loading state.

    The loading state is built in to Editor Lite, making spinners and loaders rarely needed in HTML. To benefit the built-in loading state, set the data-editor-loading property on the HTML element that should contain the editor. When bootstrapping, Editor Lite shows the loader automatically.

    Editor Lite: loading state

  2. Update syntax highlighting if the filename changes.

    // fileNameEl here is the HTML input element that contains the file name
    fileNameEl.addEventListener('change', () => {
      this.editor.updateModelLanguage(fileNameEl.value);
    });
    
  3. Get the editor’s content.

    We may set up listeners on the editor for every change, but it rapidly can become an expensive operation. Instead, get the editor’s content when it’s needed. For example, on a form’s submission:

    form.addEventListener('submit', () => {
      my_content_variable = this.editor.getValue();
    });
    
  4. Performance

    Even though Editor Lite itself is extremely slim, it still depends on Monaco editor, which adds weight. Every time you add Editor Lite to a view, the JavaScript bundle’s size significantly increases, affecting your view’s loading performance. We recommend you import the editor on demand if either:

    • You’re uncertain if the view needs the editor.
    • The editor is a secondary element of the view.

    Loading Editor Lite on demand is handled like loading any other module:

    someActionFunction() {
      import(/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite').
        then(({ default: EditorLite }) => {
          const editor = new EditorLite();
          ...
        });
      ...
    }
    

Extensions

Editor Lite provides a universal, extensible editing tool to the whole product, and doesn’t depend on any particular group. Even though the Editor Lite’s core is owned by Create::Editor FE Team, any group can own the extensions—the main functional elements. The goal of Editor Lite extensions is to keep the editor’s core slim and stable. Any needed features can be added as extensions to this core. Any group can build and own new editing features without worrying about changes to Editor Lite breaking or overriding them.

You can depend on other modules in your extensions. This organization helps keep the size of Editor Lite’s core at bay by importing dependencies only when needed.

Structurally, the complete implementation of Editor Lite can be presented as this diagram:

graph TD; B[Extension 1]---A[Editor Lite] C[Extension 2]---A[Editor Lite] D[Extension 3]---A[Editor Lite] E[...]---A[Editor Lite] F[Extension N]---A[Editor Lite] A[Editor Lite]---Z[Monaco]

An extension is an ES6 module that exports a JavaScript object:

import { Position } from 'monaco-editor';

export default {
  navigateFileStart() {
    this.setPosition(new Position(1, 1));
  },
};

In the extension’s functions, this refers to the current Editor Lite instance. Using this, you get access to the complete instance’s API, such as the setPosition() method in this particular case.

Using an existing extension

Adding an extension to Editor Lite’s instance requires the following steps:

import EditorLite from '~/editor/editor_lite';
import MyExtension from '~/my_extension';

const editor = new EditorLite().createInstance({
  ...
});
editor.use(MyExtension);

Creating an extension

Let’s create our first Editor Lite extension. Extensions are ES6 modules exporting a basic Object, used to extend Editor Lite’s features. As a test, let’s create an extension that extends Editor Lite with a new function that, when called, outputs the editor’s content in alert.

~/my_folder/my_fancy_extension.js:

export default {
  throwContentAtMe() {
    alert(this.getValue());
  },
};

In the code example, this refers to the instance. By referring to the instance, we can access the complete underlying Monaco editor API, which includes functions like getValue().

Now let’s use our extension:

~/my_folder/component_bundle.js:

import EditorLite from '~/editor/editor_lite';
import MyFancyExtension from './my_fancy_extension';

const editor = new EditorLite().createInstance({
  ...
});
editor.use(MyFancyExtension);
...
someButton.addEventListener('click', () => {
  editor.throwContentAtMe();
});

First of all, we import Editor Lite and our new extension. Then we create the editor and its instance. By default Editor Lite has no throwContentAtMe method. But the editor.use(MyFancyExtension) line brings that method to our instance. After that, we can use it any time we need it. In this case, we call it when some theoretical button has been clicked.

This script would result in an alert containing the editor’s content when someButton is clicked.

Editor Lite new extension's result

Tips

  1. Performance

    Just like Editor Lite itself, any extension can be loaded on demand to not harm loading performance of the views:

    const EditorPromise = import(
      /* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite'
    );
    const MarkdownExtensionPromise = import('~/editor/editor_markdown_ext');
    
    Promise.all([EditorPromise, MarkdownExtensionPromise])
      .then(([{ default: EditorLite }, { default: MarkdownExtension }]) => {
        const editor = new EditorLite().createInstance({
          ...
        });
        editor.use(MarkdownExtension);
      });
    
  2. Using multiple extensions

    Just pass the array of extensions to your use method:

    editor.use([FileTemplateExtension, MyFancyExtension]);