Commit 24a5859a authored by Denys Mishunov's avatar Denys Mishunov Committed by Phil Hughes

Added Monaco-based editor lite version

This introduces the bare-minimum Monaco editor for single-file edits
like those for Snippets, blobs and CI/CD configurations
parent 60c1dcb4
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import gitlabTheme from '~/ide/lib/themes/gl_theme';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { clearDomElement } from './utils';
export default class Editor {
constructor(options = {}) {
this.editorEl = null;
this.blobContent = '';
this.blobPath = '';
this.instance = null;
this.model = null;
this.options = {
...defaultEditorOptions,
...options,
};
Editor.setupMonacoTheme();
}
static setupMonacoTheme() {
monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
monacoEditor.setTheme('gitlab');
}
createInstance({ el = undefined, blobPath = '', blobContent = '' } = {}) {
if (!el) return;
this.editorEl = el;
this.blobContent = blobContent;
this.blobPath = blobPath;
clearDomElement(this.editorEl);
this.model = monacoEditor.createModel(
this.blobContent,
undefined,
new Uri('gitlab', false, this.blobPath),
);
monacoEditor.onDidCreateEditor(this.renderEditor.bind(this));
this.instance = monacoEditor.create(this.editorEl, this.options);
this.instance.setModel(this.model);
}
dispose() {
return this.instance && this.instance.dispose();
}
renderEditor() {
delete this.editorEl.dataset.editorLoading;
}
updateModelLanguage(path) {
if (path === this.blobPath) return;
this.blobPath = path;
const ext = `.${path.split('.').pop()}`;
const language = monacoLanguages
.getLanguages()
.find(lang => lang.extensions.indexOf(ext) !== -1);
const id = language ? language.id : 'plaintext';
monacoEditor.setModelLanguage(this.model, id);
}
getValue() {
return this.model.getValue();
}
}
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default () => ({
clearDomElement,
});
......@@ -8,20 +8,13 @@ import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
function setupMonacoTheme() {
monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
monacoEditor.setTheme('gitlab');
}
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default class Editor {
static create(options = {}) {
if (!this.editorInstance) {
......
import { editor as monacoEditor, Uri } from 'monaco-editor';
import Editor from '~/editor/editor_lite';
describe('Base editor', () => {
let editorEl;
let editor;
const blobContent = 'Foo Bar';
const blobPath = 'test.md';
const uri = new Uri('gitlab', false, blobPath);
const fakeModel = { foo: 'bar' };
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new Editor();
});
afterEach(() => {
editor.dispose();
editorEl.remove();
});
it('initializes Editor with basic properties', () => {
expect(editor).toBeDefined();
expect(editor.editorEl).toBe(null);
expect(editor.blobContent).toEqual('');
expect(editor.blobPath).toEqual('');
});
it('removes `editor-loading` data attribute from the target DOM element', () => {
editor.createInstance({ el: editorEl });
expect(editorEl.dataset.editorLoading).toBeUndefined();
});
describe('instance of the Editor', () => {
let modelSpy;
let instanceSpy;
let setModel;
let dispose;
beforeEach(() => {
setModel = jasmine.createSpy();
dispose = jasmine.createSpy();
modelSpy = spyOn(monacoEditor, 'createModel').and.returnValue(fakeModel);
instanceSpy = spyOn(monacoEditor, 'create').and.returnValue({
setModel,
dispose,
});
});
it('does nothing if no dom element is supplied', () => {
editor.createInstance();
expect(editor.editorEl).toBe(null);
expect(editor.blobContent).toEqual('');
expect(editor.blobPath).toEqual('');
expect(modelSpy).not.toHaveBeenCalled();
expect(instanceSpy).not.toHaveBeenCalled();
expect(setModel).not.toHaveBeenCalled();
});
it('creates model to be supplied to Monaco editor', () => {
editor.createInstance({ el: editorEl, blobPath, blobContent });
expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, uri);
expect(setModel).toHaveBeenCalledWith(fakeModel);
});
it('initializes the instance on a supplied DOM node', () => {
editor.createInstance({ el: editorEl });
expect(editor.editorEl).not.toBe(null);
expect(instanceSpy).toHaveBeenCalledWith(editorEl, jasmine.anything());
});
});
describe('implementation', () => {
beforeEach(() => {
editor.createInstance({ el: editorEl, blobPath, blobContent });
});
afterEach(() => {
editor.model.dispose();
});
it('correctly proxies value from the model', () => {
expect(editor.getValue()).toEqual(blobContent);
});
it('is capable of changing the language of the model', () => {
const blobRenamedPath = 'test.js';
expect(editor.model.getLanguageIdentifier().language).toEqual('markdown');
editor.updateModelLanguage(blobRenamedPath);
expect(editor.model.getLanguageIdentifier().language).toEqual('javascript');
});
it('falls back to plaintext if there is no language associated with an extension', () => {
const blobRenamedPath = 'test.myext';
const spy = spyOn(console, 'error');
editor.updateModelLanguage(blobRenamedPath);
expect(spy).not.toHaveBeenCalled();
expect(editor.model.getLanguageIdentifier().language).toEqual('plaintext');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment