IDE нормального человека или почему мы выбрали Monaco

Памятка от редактора


В прошлой статье мы рассказали про релиз панели управления Voximplant, не забыв упомянуть обновленную IDE. Сегодня мы посвящаем этому инструменту отдельный лонгрид — наша коллега Geloosa заботливо описала как процесс выбора технологии, так и имплементацию с вкладками, автокомплитом и кастомными стилями. Садитесь удобнее, отложите остальные дела и заходите в подкат, где любопытных ждут кишки Monaco — не поскользнитесь, их там много :) Приятного чтения.

t1vr8xlaaladvtlhhi47hm4ihsg.png


Какую библиотеку выбрать для редактора кода?


Npm выдает 400+ результатов по запросу «code editor». По большей части это UI-обертки нескольких самых популярных либ, сделанные для определенного фреймворка или проекта, плагины для тех же либ или их форки с доработками под себя, а также либы не для редактирования кода в браузере, просто попавшие в выдачу по ключевым словам. Так, к счастью, выбор значительно сужается. Еще несколько либ — а-ля CodeFlask, легковесные, но малофункциональные, предназначенные для небольших сниппетов и интерактивных примеров, но не для полноценной веб-IDE с функциональностью, к которой мы привыкли в десктопных редакторах.

В конечном итоге у нас осталось 3 библиотеки на выбор: Ace, CodeMirror и Monaco Editor. Самая ранняя из них — CodeMirror — была частной инициативой берлинца Марина Хавербеке (Marijn Haverbeke), которому понадобился редактор кода для упражнений в его онлайн-учебнике Eloquent JavaScript. Первая версия редактора выпущена в 2007 году. В 2010-м на JSConf.eu в том же Берлине была представлена первая версия Ace, который тогда разрабатывала Ajax.org для своей облачной IDE Cloud9 (собственно, Ace и расшифровывается как Ajax.org Cloud9 Editor). В 2016-м Cloud9 был куплен Амазоном и сейчас является частью AWS. Самый поздний, Monaco Editor, является компонентом VS Code и опубликован Microsoft в конце 2015-го.

У каждого редактора есть свои сильные и слабые стороны, каждый используется не в одном крупном проекте. К примеру, CodeMirror используется в инструментах разработчика Chrome и Firefox, IDE в Bitbucket, в RunKit у npm; Ace — в Codecademy, Khan Academy, MODX; Monaco — в IDE GitLab и CodeSandbox. Ниже приведена сравнительная таблица, которая, возможно, поможет вам выбрать библиотеку, наиболее подходящую для вашего проекта.

Библиотеки
Ace CodeMirror Monaco
Разработчик Cloud9 IDE (Ajax.org),
ныне — часть AmazonMozilla
Marijn Haverbeke Microsoft
Поддержка браузеров Firefox ^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
Поддержка языков
(подсветка синтаксиса)
>120 >100 >20
Кол-во символов в
последних версиях на
cndjs.com
366 608 (v1.4.3) 394 269 (v5.44.0) 2 064 949 (v0.16.2)
Вес последних версий,
gzip
2.147 KB 1.411 KB 10.898 KB
Рендеринг DOM DOM DOM и частично
(для скролла и minimap)
Документация 7 из 10: нет поиска, не всегда понятно,
что возвращают методы, есть сомнения
в полноте и актуальности
(в доке работают не все ссылки)
6 из 10: слита с юзергайдом,
поиск по Ctrl+F,
есть сомнения в полноте
9 из 10: красивая, с поиском и
перекрестными ссылками
-1 балл за отсутствие пояснений
к некоторым флагам, применение которых
не вполне очевидно из названия
Quickstart, демки How-to — текстовые документы с примерами кода,
отдельно есть демки с примерами кода
(правда, они разбросаны по разным страницам,
не все работают и ищутся они проще всего через гугл),
есть демка, где можно пощупать разные фичи,
но управлять ими предлагается через UI-контролы,
то есть потом надо еще отдельно искать методы
для их подключения
How-to прямо-таки бедные,
в основном все разбросано по github
и stackoverflow, зато есть демки фич с примерами
кода для их реализации
Объединены в формате плейграунда:
код с комментами и рядом демо, можно
сразу попробовать и оценить
многие возможности
Активность сообщества Средняя Высокая Средняя
Активность разработчиков Средняя Средняя Высокая


Бессмысленно сравнивать библиотеки по размеру, потому что все зависит от того, что и как подключать для конкретного проекта: грузить готовый файл с одним из билдов (которые тоже разнятся) или прогонять npm-пакет через какой-то сборщик? А самое важное — в каком объеме используется редактор: подгружаются ли все стили и темы, сколько и каких аддонов и плагинов использовано. Например, в CodeMirror большая часть функциональности, которая работает в Monaco и Ace из коробки, доступна только с аддонами. В таблице приведено количество символов в последних версиях на CDN и вес их сжатых файлов для общего представления, о каких порядках идет речь.

Во всех библиотеках примерно одинаковый набор базовых фич: автоформатирование кода, сворачивание строк, cut/copy/paste, горячие клавиши, возможность добавления новых синтаксисов для подсветки и тем, проверка синтаксиса (в CodeMirror — только через аддоны, в Ace — пока только для JavaScript/CoffeeScript/CSS/XQuery), подсказки и автокомплит (в CodeMirror — через аддоны), продвинутый поиск по коду (в CodeMirror — через аддоны), методы для реализации табов и сплит-режима, дифф-режим и инструмент для мержа (в CodeMirror — либо с плюсам и минусами в одном окне, либо двухпанельный через аддон, в Ace — отдельная либа). Для CodeMirror в силу его возраста написано много аддонов, но их количество будет влиять и на вес, и на скорость редактора. Monaco многое умеет из коробки, причем, на мой взгляд, лучше и в большем объеме, чем Ace и CodeMirror.

Мы остановились на Monaco по нескольким причинам:

  1. Наиболее развиты инструменты, которые мы сочли критично важными для нашего проекта:
    • IntelliSense — подсказки и автокомплит;
    • умная навигация по коду в контекстном меню и через minimap;
    • двухпанельный дифф-режим из коробки.

  2. Написан на TypeScript. Наша панель управления написана на Vue+Typescript, поэтому поддержка TS была важна. К слову, Ace с недавнего времени тоже поддерживает TS, но изначально он был написан на JS. Для CodeMirror есть типы в DefinitelyTyped.
  3. В нем наиболее активно идет разработка (возможно, потому что он вышел не так давно), быстрее правятся баги и мержатся пул-реквесты. Для сравнения, с CodeMirror у нас был печальный опыт, когда баги не правились годами и мы ставили костыль на костыле и костылем погоняли.
  4. Удобная автосгенеренная (что дает надежду на ее полноту) документация с перекрестными ссылками между интерфейсами и методами.
  5. На наш вкус, наиболее красивый UI (наверное, тоже связано с временем создания) и лаконичный API.
  6. Поспрашивав знакомых разработчиков, какой из редакторов вызывал больше головной боли, в лидерах оказались Ace и CodeMirror.


Отдельно стоит сказать про скорость работы. Затратный синтаксический анализ происходит в параллельном потоке воркера. Плюс все вычисления ограничиваются размером вьюпорта (все типы, цвета, отрисовка рассчитываются только для тех строк, которые видны). Тормозить начинает, только если коде под 100 000 строк — подсказки могут вычисляться по несколько секунд. Ace, который тоже использует воркеры для тяжелых вычислений, оказался быстрее: в коде такой же длины подсказки появляются практически моментально, да и с 200 000 строками он быстро справляется (на официальном сайте заявлено, что даже 4 млн строк не должны оказаться проблемой, хотя у меня разогнались винты, стал тормозить ввод и исчезли подсказки после 1-го миллиона). CodeMirror, где параллельных вычислений нет, совсем с трудом тянет такие объемы: может мелькать и текст, и подсветка синтаксиса. Поскольку в реальном мире 100 000 строк в файле — редкость, мы закрыли на это глаза. Даже с 40–50 тысячами строк Monaco справляется прекрасно.

Подключение Monaco и использование основных фич (на примере интеграции с Vue)


Подключение


Здесь я буду давать примеры кода из vue-компонентов и использовать соответствующую терминологию. Но все это легко переносится в любой другой фреймворк или чистый JS.

Исходник Monaco можно скачать на официальном сайте и положить себе в проект, можно забрать с CDN, можно подключить к проекту через npm. Я расскажу про третий вариант и сборку с помощью webpack.

Ставим monaco-editor и плагин для сборки:

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


В конфиг вебпака добавляем:

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    // ...
    new MonacoWebpackPlugin()
  ]
};


Если вы используете Vue и vue-cli-service для сборки, добавляем во vue.config.js:

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

module.exports = {
  // ...
  configureWebpack: (config) => {
    // ...
    config.plugins.push(new MonacoWebpackPlugin());
  }
};


Если вам не нужны все языки и фичи Monaco, для уменьшения размера бандла можно передать в MonacoWebpackPlugin объект с настройками:

new MonacoWebpackPlugin({
  output: '', // папка, куда собирать скрипты воркеров
  languages: ['markdown'], // массив строк с названиями языков, для которых нужна подсветка 
  features: ['format', 'contextmenu'] // массив строк с нужными фичами
})


Полный список фич и языков для плагина здесь.

Создаем и настраиваем редактор


Импортируем editor и вызываем editor.create(el: HTMLElement, config?: IEditorConstructionOptions), передавая в качестве первого аргумента элемент DOM, в котором хотим создать редактор.

В компоненте редактора:







Контейнеру для редактора нужно обязательно задавать высоту, чтобы она не оказалась нулевой. Если вы создадите редактор в пустом div-е (с нулевой высотой — ваш К.О.), Monaco пропишет такую же высоту инлайн-стилем у окна редактора.

Второй необязательный аргумент editor.create — конфиг редактора. В нем более сотни опций, полное описание интерфейса IEditorConstructionOptions есть в документации.
Для примера зададим язык, тему и изначальный текст и включим перенос строк (по дефолту они не переносятся):

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

this.editor = editor.create(this.$refs.editor, config);


Функция editor.create возвращает объект с интерфейсом IStandaloneCodeEditor. Через него теперь можно управлять всем происходящим в редакторе, в том числе изменять первоначальные настройки:

// выключаем перенос строк и переключаем редактор в read-only режим
this.editor.updateOptions({wordWrap: 'off', readOnly: true}); 


Теперь о боли: updateOptions принимает объект с интерфейсом IEditorOptions, а не IEditorConstructionOptions. Они немного отличаются: IEditorConstructionOptions шире, в него входят свойства данного инстанса редактора и некоторые глобальные. Свойства инстанса меняются через updateOptions, глобальные — через методы глобального editor. И соответственно, те, что меняются глобально, меняются для всех инстансов. Среди таких параметров — theme. Создадим 2 инстанса с разными темами; y обоих будет та, которая задана в последнем (здесь — темная). Глобальный метод editor.setTheme('vs') также сменит тему у обоих. Это скажется даже на тех окнах, что находятся на другой странице вашего SPA. Таких мест немного, но за ними надо следить.




Удаление редактора


При уничтожении окна Monaco надо вызвать метод dispose, иначе не очистятся все листенеры и созданные после этого окна могут работать некорректно, реагируя на некоторые события по несколько раз:

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


Вкладки


Вкладки открытых в редакторе файлов используют одно и то же окно Monaco. Для переключения между ними используются методы IStandaloneCodeEditor: getModel для сохранения и setModel для обновления модели редактора. Модель хранит текст, позицию курсора, историю действий для undo-redo. Для создания модели нового файла используется глобальный метод editor.createModel(text: string, language: string). Если файл пустой, можно не создавать модель и передать null в setModel:

Посмотреть код





Дифф-режим


Для дифф-режима нужно использовать другой метод editor при создании окна редактора — createDiffEditor:


// ...
mounted() {
  this.diffEditor = editor.createDiffEditor(this.$refs.diffEditor, config);
}
// ...


Он принимает те же параметры, что editor.create, но конфиг должен иметь интерфейс IDiffEditorConstructionOptions, который несколько отличается от конфига обычного редактора, в частности, в нем нет value. Тексты для сравнения задаются после создания через setModel возвращенного IStandaloneDiffEditor:

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

Контекстное меню, палитра команд и горячие клавиши


Monaco использует свое, не браузерное, контекстное меню, где есть умная навигация, мультикурсор для изменения всех вхождений и командная палитра как в VS Code (Command palette) с кучей полезных команд и горячих клавиш, ускоряющих написание кода:

kaobfagb3sdmtmikvocuai96die.png
                               Monaco context menu


eg2s_c1kzyqi0eo4yhxo2o-sde4.png
                               Monaco command palette

Контекстное меню расширяется через метод addAction (он есть и в IStandaloneCodeEditor, и в IStandaloneDiffEditor), принимающий объект IActionDescriptor:

Посмотреть код
// ...  
// ... // импортируем 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; }


Чтобы только привязать сочетание клавиш к действию, не показывая его в контекстном меню, используется тот же метод, только не указывается contextMenuGroupId у действия:

Посмотреть код
// ...
// кастомные действия
private myActions = [
  {
    contextMenuGroupId: '1_modification',
    contextMenuOrder: 3,
    label: 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); // добавляем все кастомные действия 
}


В палитру команд попадут все добавленные действия.

Подсказки и автокомплит


Для этих целей в Monaco использован IntelliSense, что круто. По ссылке можно почитать и посмотреть на скринах, сколько полезной инфы он умеет показывать. Если для вашего языка еще нет автокомплита, его можно добавить через registerCompletionItemProvider. А для JS и TS уже есть метод addExtraLib, позволяющей загрузить определения на TypeScript для подсказок и автокомплита:

// ...
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();
}


В первом параметре строкой передаются определения, во втором, необязательном, — название либы.

Кастомные языки и темы


В Monaco есть модуль Monarch для определения синтаксиса своих языков. Синтаксис описывается вполне стандартно: задается соответствие между регулярками и токенами, характерными для данного языка.

Посмотреть код
// ...
// описываем язык, синтаксис которого состоит из:
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);
  // ...
}


Также для своих токенов можно создать тему — объект с интерфейсом IStandaloneThemeData — и установить ее в глобальный 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);
  // ...
}


Теперь текст на описанном языке будет выглядеть так:

gvb-3-3by31loig9rdgmjoy0kb8.png


Применять эту фичу можно, насколько хватит фантазии. Например, мы сделали в своей панели для разработчиков просмотрщик логов звонков. Логи зачастую длинные и непонятные, но когда они показываются с подсветкой синтаксиса, умным поиском, сворачиванием/разворачиванием строк, нужными командами (вроде приттифаера), выделением всех строк звонка по его id или переводом времени в логе в другой часовой пояс, то копаться в них становится намного проще (скриншот кликабелен):

4i5vrkhe7g__s_mecfpytcf1gra.png


Заключение


Резюмируя, скажу, что Monaco — огонь. После нескольких месяцев работы с ним у меня исключительно приятные воспоминания. Если вы выбираете редактор для кода, обязательно зайдите на его Playground и поиграйтесь с кодом, посмотрите, что еще он умеет. Возможно, это именно то, что вы ищете.

© Habrahabr.ru