📜 ⬆️ ⬇️

Idea normal person or why we chose Monaco

Editor's Memo


In the last article, we talked about the release of the Voximplant control panel, without forgetting to mention the updated IDE. Today we dedicate to this tool a separate longrid - our colleague Geloosa carefully described both the process of choosing a technology, and the implementation with tabs, autocomplete and custom styles. Sit down more conveniently, set aside the rest of the case and go into the tackle, where Monaco guts are waiting for the curious - do not slip, there are many of them :) Pleasant reading.


Which library to choose for the code editor?


Npm produces 400+ results for the code editor. For the most part, these are the UI-wrappers of several of the most popular libs made for a specific framework or project, plug-ins for the same libs or their forks with customizations, and also not for editing the code in the browser that just got into the output by keywords. So, fortunately, the choice is significantly narrowed. A few more libs à la CodeFlask , lightweight but low-functional, designed for small snippets and interactive examples, but not for a full-fledged web-IDE with the functionality we are used to in desktop editors.

In the end, we have 3 libraries left to choose from: Ace , CodeMirror and Monaco Editor . The earliest of these, CodeMirror, was a private initiative of Berliner Marina Haverbeke ( Marijn Haverbeke ), who needed a code editor for the exercises in his online tutorial Eloquent JavaScript . The first version of the editor was released in 2007. In 2010, at JSConf.eu in the same Berlin, the first version of Ace was presented, which then developed Ajax.org for its cloud-based IDE Cloud9 (actually, Ace stands for Ajax.org Cloud9 Editor). In 2016, Cloud9 was bought by Amazon and is now part of AWS. The latest, Monaco Editor, is a component of VS Code and published by Microsoft at the end of 2015.
')
Each editor has its strengths and weaknesses, each is used in more than one large project. For example, CodeMirror is used in Chrome and Firefox developer tools, IDE in Bitbucket, in RunKit for npm; Ace - in Codecademy, Khan Academy, MODX; Monaco - in IDE GitLab and CodeSandbox. Below is a comparative table that may help you choose the library that is most suitable for your project.

Libraries
AceCodemirrorMonaco
DeveloperCloud9 IDE (Ajax.org),
now part of AmazonMozilla
Marijn haverbekeMicrosoft
Browser supportFirefox ^ 3.5
Chrome
Safari ^ 4.0
IE ^ 8.0
Opera ^ 11.5
Firefox ^ 3.0
Chrome
Safari ^ 5.2
IE ^ 8.0
Opera ^ 9.2
Firefox ^ 4.0
Chrome
Safari (v -?)
IE ^ 11.0
Opera ^ 15.0
Language support
(syntax highlighting)
> 120> 100> 20
Number of characters in
latest versions on
cndjs.com
366 608 (v1.4.3)394,269 (v5.44.0)2,064,949 (v0.16.2)
The weight of the latest versions,
gzip
2.147 KB1.411 KB10.898 KB
RenderingDomDomDOM and partly <canvas>
(for scrolling and minimap)
Documentation7 out of 10: no search, not always clear
That methods are returned, there are doubts
in completeness and relevance
(not all links work in the dock)
6 out of 10: merged with the user,
Search Ctrl + F,
there are doubts about completeness
9 out of 10: beautiful, with search and
cross-references
-1 point for the lack of explanation
to some flags whose use
not quite obvious from the title
Quickstart demosHow-to - text documents with code examples,
separately there are demos with code examples
(although they are scattered across different pages,
not everyone works and they are most easily searched through Google),
there is a demo where you can feel different features,
but it is proposed to control them through UI controls,
that is, then you need to separately look for methods
to connect them
How-to downright poor
basically everything is scattered on github
and stackoverflow, but there are feature demos with examples
code to implement them
Merged in playground format:
code with comments and a number of demos, you can
immediately try and evaluate
many possibilities
Community activityAverageHighAverage
Developer ActivityAverageAverageHigh

It makes no sense to compare libraries by size, because everything depends on what and how to connect for a specific project: load the finished file with one of the builds (which also differ) or run the npm package through some kind of collector. And the most important thing is the extent to which the editor is used: whether all styles and themes are loaded, how many and which add-ons and plug-ins are used. For example, in CodeMirror most of the functionality that works in Monaco and Ace out of the box is available only with addons. The table shows the number of characters in the latest versions on CDN and the weight of their compressed files for a general idea of ​​what orders we are talking about.

All libraries have approximately the same set of basic features: code auto-formatting, line folding, cut / copy / paste, hot keys, the ability to add new syntaxes for highlighting and themes, syntax checking (in CodeMirror - only through addons, in Ace - only for JavaScript / CoffeeScript / CSS / XQuery), hints and autocompletes (in CodeMirror through addons), advanced search by code (in CodeMirror through addons), methods for implementing tabs and split mode, diff mode and tool for merge (in CodeMirror - either with pluses and minuses in one window, or two-panel through the addon, Ace - Separate Lieb). For CodeMirror, due to its age, many add-ons are written, but their number will affect both the weight and the speed of the editor. Monaco is able to do a lot out of the box, and, in my opinion, better and in greater volume than Ace and CodeMirror.

We stopped at Monaco for several reasons:

  1. The most developed tools that we considered crucial for our project:
    • IntelliSense - tips and autocomplete;
    • smart code navigation in the context menu and via minimap;
    • two-pane diff-mode out of the box.

  2. Written in TypeScript. Our control panel is written in Vue + Typescript, so TS support was important. By the way, Ace has recently supported TS, but initially it was written in JS. For CodeMirror, there are types in DefinitelyTyped .
  3. It is most actively developing (perhaps because it was released not so long ago), bugs rule more quickly, and pull requests are merging. For comparison, with CodeMirror, we had a sad experience when bugs did not rule over the years and we put a crutch on a crutch and chased it with a crutch.
  4. Convenient, autogenerated (which gives hope for its completeness) documentation with cross-references between interfaces and methods.
  5. To our taste, the most beautiful UI (probably also related to the time of creation) and a concise API.
  6. After asking friends of developers, which of the editors caused more headaches, Ace and CodeMirror were the leaders.

We should also say about the speed of work. Cost parsing occurs in a parallel worker thread. Plus, all calculations are limited by the size of the viewport (all types, colors, drawing are calculated only for those rows that are visible). It begins to brake only if the code is under 100,000 lines - hints can be calculated for several seconds. Ace, who also uses hard workers for heavy calculations, turned out to be faster: the prompts appear almost instantly in the code of the same length, and he quickly copes with 200,000 lines (the official site says that even 4 million lines should not be a problem, although I was overclocked screws, began to slow down the input and tips disappeared after the 1st million). CodeMirror, where there is no parallel computing, is quite difficult to draw such volumes: it can flicker the text, and syntax highlighting. Since in the real world 100,000 lines in the file are rare, we closed our eyes to this. Even with 40-50 thousand lines Monaco copes perfectly.

Connecting Monaco and using basic features (for example, integration with Vue)


Connection


Here I will give code examples from vue components and use appropriate terminology. But all this is easily transferred to any other framework or pure JS.

Monaco source code can be downloaded on the official website and put in a project, you can pick up from a CDN, you can connect to the project via npm. I will tell about the third option and the assembly by means of webpack.

We put monaco-editor and plugin for the assembly:

npm i -S monaco-editor npm i -D monaco-editor-webpack-plugin 

We add to the webcam configuration:

 const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); module.exports = { // ... plugins: [ // ... new MonacoWebpackPlugin() ] }; 

If you use Vue and vue-cli-service for the build, add to vue.config.js:

 const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); module.exports = { // ... configureWebpack: (config) => { // ... config.plugins.push(new MonacoWebpackPlugin()); } }; 

If you do not need all the languages ​​and features of Monaco, to reduce the size of the bundle, you can transfer to the MonacoWebpackPlugin object with the settings:

 new MonacoWebpackPlugin({ output: '', // ,     languages: ['markdown'], //     ,     features: ['format', 'contextmenu'] //      }) 

The complete list of features and languages ​​for the plugin is here .

Create and customize editor


We import the editor and call editor.create(el: HTMLElement, config?: IEditorConstructionOptions) , passing as the first argument the DOM element in which we want to create an editor.

In the editor component:

 <template> <div ref='editor' class='editor'></div> </template> <script> import {editor} from 'monaco-editor'; import {Component, Prop, Vue} from 'vue-property-decorator'; @Component() export default class Monaco extends Vue { private editor = null; mounted() { this.editor = editor.create(this.$refs.editor); } } </script> <style> .editor { margin: auto; width: 60vw; height: 200px; } </style> 

The container for the editor must set the height so that it does not turn out to be zero. If you create an editor in an empty div (with zero height — your KO), Monaco will prescribe the same height inline style at the editor window.

The second optional argument of editor.create is the editor's config. It has more than a hundred options, a full description of the IEditorConstructionOptions interface is in the documentation.

For example, let's set the language, the subject and the original text and turn on line wrapping (they are not transferred by default):

 const config = { value: `function hello() { alert('Hello world!'); }`, language: 'javascript', theme: 'vs-dark', wordWrap: 'on' }; this.editor = editor.create(this.$refs.editor, config); 

The editor.create function returns an object with the IStandaloneCodeEditor interface. Through it, you can now manage everything that happens in the editor, including changing the initial settings:

 //        read-only  this.editor.updateOptions({wordWrap: 'off', readOnly: true}); 

Now for the pain: updateOptions takes an object with the IEditorOptions interface, not IEditorConstructionOptions. They are a little different: IEditorConstructionOptions is wider, it includes properties of this instance of the editor and some global ones. The properties of the instance are changed via updateOptions , global - through the methods of the global editor . And accordingly, those that change globally, change for all instances. Among such parameters is the theme . Create 2 instances with different themes; y both will be the one given in the last one (here it is dark). The global editor.setTheme('vs') method will also change the subject for both. This will affect even those windows that are on another page of your SPA. There are few such places, but you have to keep an eye on them.

 <template> <div ref='editor1' class='editor'></div> <div ref='editor2' class='editor'></div> </template> <script> // ... this.editor1 = editor.create(this.$refs.editor1, {theme: 'vs'}); this.editor2 = editor.create(this.$refs.editor2, {theme: 'vs-dark'}); // ... </script> 


Delete Editor


When the Monaco window is destroyed, the dispose method must be called, otherwise all listeners will not be cleared and the windows created after this may not work correctly, reacting to certain events several times:

 beforeDestroy() { this.editor && this.editor.dispose(); } 

Tabs


The tabs of files opened in the editor use the same Monaco window. To switch between them, use the methods IStandaloneCodeEditor: getModel for saving and setModel for updating the editor model. The model stores text, cursor position, history of actions for undo-redo. To create a new file model, the global method editor.createModel(text: string, language: string) . If the file is empty, you can not create a model and pass null to setModel :

View code
 <template> <div class='tabs'> <div class='tab' v-for="tab in tabs" :key'tab.id' @click='() => switchTab(tab.id)'> {{tab.name}} </div> </div> <div ref='editor' class='editor'></div> </template> <script> import {editor} from 'monaco-editor'; import {Component, Prop, Vue} from 'vue-property-decorator'; @Component() export default class Monaco extends Vue { private editor = null; private tabs: [ {id: 1, name: 'tab 1', text: 'const tab = 1;', model: null, active: true}, {id: 2, name: 'tab 2', text: 'const tab = 2;', model: null, active: false} ]; mounted() { this.editor = editor.create(this.$refs.editor); } private switchTab(id) { const activeTab = this.tabs.find(tab => tab.id === id); if (!activeTab.active) { //    (     )    const model = !activeTab.model && activeTab.text ? editor.createModel(activeTab.text, 'javascript') : activeTab.model; //          this.tabs = this.tabs.map(tab => ({ ...tab, model: tab.active ? this.editor.getModel() : tab.model, active: tab.id === id })); //    this.editor.setModel(model); } } </script> 


Diff mode


For the diff mode, you need to use another editor method when creating the editor window - createDiffEditor :

 <template> <div ref='diffEditor' class='editor'></div> </template> // ... mounted() { this.diffEditor = editor.createDiffEditor(this.$refs.diffEditor, config); } // ... 

It takes the same parameters as editor.create , but the config must have the IDiffEditorConstructionOptions interface, which is somewhat different from the configuration of the regular editor, in particular, it has no value . Texts for comparison are set after creating the returned IStandaloneDiffEditor via setModel :

 this.diffEditor.setModel({ original: editor.createModel('const a = 1;', 'javascript'), modified: editor.createModel('const a = 2;', 'javascript') }); 


Context menu, command palette and hotkeys


Monaco uses its non-browser-based context menu, where there is smart navigation, a multi-cursor for changing all occurrences and a command palette as in VS Code (Command palette) with a bunch of useful commands and hot keys that speed up writing code:

  Monaco context menu 


  Monaco command palette 


The context menu is extended through the addAction method (both in IStandaloneCodeEditor and IStandaloneDiffEditor ), which accepts an IActionDescriptor object:

View code
 // ... <div ref='diffEditor' :style='{display: isDiffOpened ? "block" : "none"}'></div> // ... //  KeyCode  KeyMod     import {editor, KeyCode, KeyMod} from "monaco-editor"; // ... private editor = null; private diffEditor = null; private isDiffOpened = false; private get activeTab() { return this.tabs.find(tab => tab.active); } mounted() { this.diffEditor = editor.createDiffEditor(this.$refs.diffEditor); this.editor = editor.create(this.$refs.editor); this.editor.addAction({ //  ,     . contextMenuGroupId: '1_modification', //   : 1 - 'navigation', 2 - '1_modification', 3 - '9_cutcopypaste'; //    contextMenuOrder: 3, //       label: 'Show diff', id: 'showDiff', keybindings: [KeyMod.CtrlCmd + KeyMod.Shift + KeyCode.KEY_D], //   // ,     //    run: this.showDiffEditor }); } //      private showDiffEditor() { this.diffEditor.setModel({ original: this.activeTab.initialText, modified: this.activeTab.editedText }); this.isDiffOpened = true; } 


To only bind the shortcut to an action, without showing it in the context menu, the same method is used, only the contextMenuGroupId not specified for the action:

View code
 // ... //   private myActions = [ { contextMenuGroupId: '1_modification', contextMenuOrder: 3, label: <string>this.$t('scenarios.showDiff'), id: 'showDiff', keybindings: [KeyMod.CtrlCmd + KeyMod.Shift + KeyCode.KEY_D], run: this.showDiffEditor }, // ,   Ctrl + C + L      { label: 'Get content length', id: 'getContentLength', keybindings: [KeyMod.CtrlCmd + KeyCode.Key_C + KeyCode.Key_L], run: () => this.editor && alert(this.editor.getValue().length) } ]; mounted() { this.editor = editor.create(this.$refs.editor); this.myActions.forEach(this.editor.addAction); //     } 


All added actions will get into the command palette.

Tips and autocomplete


For these purposes, Monaco used IntelliSense , which is cool. Under the link you can read and see on screenshots how much useful information he can show. If there is no autocomplete for your language yet, you can add it via registerCompletionItemProvider . And for JS and TS, there is already a addExtraLib method that allows you to load definitions into TypeScript for hints and autocomplete:

 // ... import {languages} from "monaco-editor"; // ... // ,          private myAddedLib = null; mounted() { // languages     Monaco this.myAddedLib = languages.typescript.javascriptDefaults.addExtraLib('interface MyType {prop: string}', 'myLib'); } beforeDestroy() { //  ,   this.myAddedLib && this.myAddedLib.dispose(); } 

In the first parameter, the definitions are passed in a string, in the second, optional, the name is given.

Custom languages ​​and themes


Monaco has a Monarch module for determining the syntax of its languages. The syntax is described quite standardly: the correspondence between regular and tokens characteristic for the given language is set.

View code
 // ... //  ,    : private myLanguage = { defaultToken: 'text', //  , brackets: [{ open: '(', close: ')', token: 'bracket.parenthesis' }], // ,   , keywords: [ 'autumn', 'winter', 'spring', 'summer' ], //     tokenizer: { root: [{ regex: /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/, action: { token: 'date' } }, { regex: /(boy|girl|man|woman|person)(\s[A-Za-z]+)/, action: ['text', 'variable'] } ] } }; mounted() { //     languages.register({ id: 'myLanguage' }); //      languages.setMonarchTokensProvider('myLanguage', this.myLanguage); // ... } 


You can also create a theme for your tokens — an object with the IStandaloneThemeData interface — and set it in the global editor :

 // ... private myTheme = { base: 'vs', // ,      inherit: true, //       rules: [ {token: 'date', foreground: '22aacc'}, {token: 'variable', foreground: 'ff6600'}, {token: 'text', foreground: 'd4d4d4'}, {token: 'bracket', foreground: 'd4d4d4'} ] }; mounted() { editor.defineTheme('myTheme', this.myTheme); // ... } 

Now the text in the described language will look like this:


You can apply this feature as far as imagination is enough. For example, we made a call log viewer in our developer panel. Logs are often long and incomprehensible, but when they are shown with syntax highlighting, smart search, folding / unfolding of lines, necessary commands (for example, Prettify params), highlighting all call lines by its id or transferring time in the log to a different time zone, then dig it becomes much simpler in them (the screenshot is clickable):


Conclusion


Summarizing, I will say that Monaco is a fire. After several months of working with him, I have exceptionally pleasant memories. If you choose an editor for the code, be sure to go to its Playground and play around with the code, see what else it can do. Perhaps this is exactly what you are looking for.

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


All Articles