IDE нормального человека или почему мы выбрали Monaco
- четверг, 28 марта 2019 г. в 00:21:10
Библиотеки | |||
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 и частично <canvas> (для скролла и minimap) |
Документация | 7 из 10: нет поиска, не всегда понятно, что возвращают методы, есть сомнения в полноте и актуальности (в доке работают не все ссылки) |
6 из 10: слита с юзергайдом, поиск по Ctrl+F, есть сомнения в полноте |
9 из 10: красивая, с поиском и перекрестными ссылками -1 балл за отсутствие пояснений к некоторым флагам, применение которых не вполне очевидно из названия |
Quickstart, демки | How-to – текстовые документы с примерами кода, отдельно есть демки с примерами кода (правда, они разбросаны по разным страницам, не все работают и ищутся они проще всего через гугл), есть демка, где можно пощупать разные фичи, но управлять ими предлагается через UI-контролы, то есть потом надо еще отдельно искать методы для их подключения |
How-to прямо-таки бедные, в основном все разбросано по github и stackoverflow, зато есть демки фич с примерами кода для их реализации |
Объединены в формате плейграунда: код с комментами и рядом демо, можно сразу попробовать и оценить многие возможности |
Активность сообщества | Средняя | Высокая | Средняя |
Активность разработчиков | Средняя | Средняя | Высокая |
npm i -S monaco-editor
npm i -D monaco-editor-webpack-plugin
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new MonacoWebpackPlugin()
]
};
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
// ...
configureWebpack: (config) => {
// ...
config.plugins.push(new MonacoWebpackPlugin());
}
};
MonacoWebpackPlugin
объект с настройками:new MonacoWebpackPlugin({
output: '', // папка, куда собирать скрипты воркеров
languages: ['markdown'], // массив строк с названиями языков, для которых нужна подсветка
features: ['format', 'contextmenu'] // массив строк с нужными фичами
})
editor
и вызываем editor.create(el: HTMLElement, config?: IEditorConstructionOptions)
, передавая в качестве первого аргумента элемент DOM, в котором хотим создать редактор.<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>
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. Таких мест немного, но за ними надо следить.<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>
dispose
, иначе не очистятся все листенеры и созданные после этого окна могут работать некорректно, реагируя на некоторые события по несколько раз:beforeDestroy() {
this.editor && this.editor.dispose();
}
getModel
для сохранения и setModel
для обновления модели редактора. Модель хранит текст, позицию курсора, историю действий для undo-redo. Для создания модели нового файла используется глобальный метод editor.createModel(text: string, language: string)
. Если файл пустой, можно не создавать модель и передать null
в setModel
:<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>
editor
при создании окна редактора — createDiffEditor
:<template>
<div ref='diffEditor' class='editor'></div>
</template>
// ...
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 context menu
Monaco command palette
addAction
(он есть и в IStandaloneCodeEditor
, и в IStandaloneDiffEditor
), принимающий объект IActionDescriptor:// ...
<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;
}
contextMenuGroupId
у действия:// ...
// кастомные действия
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); // добавляем все кастомные действия
}
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();
}
// ...
// описываем язык, синтаксис которого состоит из:
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);
// ...
}
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);
// ...
}