Commit baea0902 authored by Denys Mishunov's avatar Denys Mishunov Committed by Simon Knox

Abstracted some methods into static

To make createInstance more lightweight in anticipation
for the Diff Editor's implementation, we need to refactor
the createInstance a bit to make it less overloaded.
parent a6b74a6c
...@@ -11,6 +11,8 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __( ...@@ -11,6 +11,8 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an extension.', 'Editor Lite instance is required to set up an extension.',
); );
export const EDITOR_READY_EVENT = 'editor-ready';
// //
// EXTENSIONS' CONSTANTS // EXTENSIONS' CONSTANTS
// //
......
...@@ -5,7 +5,7 @@ import { defaultEditorOptions } from '~/ide/lib/editor_options'; ...@@ -5,7 +5,7 @@ import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils'; import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { clearDomElement } from './utils'; import { clearDomElement } from './utils';
import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants'; import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX, EDITOR_READY_EVENT } from './constants';
import { uuids } from '~/diffs/utils/uuids'; import { uuids } from '~/diffs/utils/uuids';
export default class EditorLite { export default class EditorLite {
...@@ -73,6 +73,48 @@ export default class EditorLite { ...@@ -73,6 +73,48 @@ export default class EditorLite {
}); });
} }
static prepareInstance(el) {
if (!el) {
throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
clearDomElement(el);
monacoEditor.onDidCreateEditor(() => {
delete el.dataset.editorLoading;
});
}
static manageDefaultExtensions(instance, el, extensions) {
EditorLite.loadExtensions(extensions, instance)
.then((modules) => {
if (modules) {
modules.forEach((module) => {
instance.use(module.default);
});
}
})
.then(() => {
el.dispatchEvent(new Event(EDITOR_READY_EVENT));
})
.catch((e) => {
throw e;
});
}
static createEditorModel({ blobPath, blobContent, blobGlobalId, instance } = {}) {
let model = null;
if (!instance) {
return null;
}
const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
const uri = Uri.file(uriFilePath);
const existingModel = monacoEditor.getModel(uri);
model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
instance.setModel(model);
return model;
}
/** /**
* Creates a monaco instance with the given options. * Creates a monaco instance with the given options.
* *
...@@ -90,25 +132,15 @@ export default class EditorLite { ...@@ -90,25 +132,15 @@ export default class EditorLite {
extensions = [], extensions = [],
...instanceOptions ...instanceOptions
} = {}) { } = {}) {
if (!el) { EditorLite.prepareInstance(el);
throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
clearDomElement(el);
const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath));
monacoEditor.onDidCreateEditor(() => {
delete el.dataset.editorLoading;
});
const instance = monacoEditor.create(el, { const instance = monacoEditor.create(el, {
...this.options, ...this.options,
...instanceOptions, ...instanceOptions,
}); });
instance.setModel(model);
const model = EditorLite.createEditorModel({ blobGlobalId, blobPath, blobContent, instance });
instance.onDidDispose(() => { instance.onDidDispose(() => {
const index = this.instances.findIndex((inst) => inst === instance); const index = this.instances.findIndex((inst) => inst === instance);
this.instances.splice(index, 1); this.instances.splice(index, 1);
...@@ -117,20 +149,7 @@ export default class EditorLite { ...@@ -117,20 +149,7 @@ export default class EditorLite {
instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance); instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance);
instance.use = (args) => this.use(args, instance); instance.use = (args) => this.use(args, instance);
EditorLite.loadExtensions(extensions, instance) EditorLite.manageDefaultExtensions(instance, el, extensions);
.then((modules) => {
if (modules) {
modules.forEach((module) => {
instance.use(module.default);
});
}
})
.then(() => {
el.dispatchEvent(new Event('editor-ready'));
})
.catch((e) => {
throw e;
});
this.instances.push(instance); this.instances.push(instance);
return instance; return instance;
......
<script> <script>
import EditorLite from '~/vue_shared/components/editor_lite.vue'; import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext'; import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EDITOR_READY_EVENT } from '~/editor/constants';
export default { export default {
components: { components: {
...@@ -31,6 +32,7 @@ export default { ...@@ -31,6 +32,7 @@ export default {
}); });
}, },
}, },
readyEvent: EDITOR_READY_EVENT,
}; };
</script> </script>
<template> <template>
...@@ -39,7 +41,7 @@ export default { ...@@ -39,7 +41,7 @@ export default {
ref="editor" ref="editor"
:file-name="ciConfigPath" :file-name="ciConfigPath"
v-bind="$attrs" v-bind="$attrs"
@editor-ready="onEditorReady" @[$options.readyEvent]="onEditorReady"
v-on="$listeners" v-on="$listeners"
/> />
</div> </div>
......
<script> <script>
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Editor from '~/editor/editor_lite'; import Editor from '~/editor/editor_lite';
import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants'; import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
function initEditorLite({ el, ...args }) { function initEditorLite({ el, ...args }) {
const editor = new Editor({ const editor = new Editor({
...@@ -88,6 +88,7 @@ export default { ...@@ -88,6 +88,7 @@ export default {
return this.editor; return this.editor;
}, },
}, },
readyEvent: EDITOR_READY_EVENT,
}; };
</script> </script>
<template> <template>
...@@ -95,7 +96,7 @@ export default { ...@@ -95,7 +96,7 @@ export default {
:id="`editor-lite-${fileGlobalId}`" :id="`editor-lite-${fileGlobalId}`"
ref="editor" ref="editor"
data-editor-loading data-editor-loading
@editor-ready="$emit('editor-ready')" @[$options.readyEvent]="$emit($options.readyEvent)"
> >
<pre class="editor-loading-content">{{ value }}</pre> <pre class="editor-loading-content">{{ value }}</pre>
</div> </div>
......
...@@ -4,7 +4,11 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -4,7 +4,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import Editor from '~/editor/editor_lite'; import Editor from '~/editor/editor_lite';
import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base'; import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from '~/editor/constants'; import {
EDITOR_LITE_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
EDITOR_READY_EVENT,
} from '~/editor/constants';
describe('Base editor', () => { describe('Base editor', () => {
let editorEl; let editorEl;
...@@ -43,16 +47,21 @@ describe('Base editor', () => { ...@@ -43,16 +47,21 @@ describe('Base editor', () => {
let instanceSpy; let instanceSpy;
let setModel; let setModel;
let dispose; let dispose;
let modelsStorage;
beforeEach(() => { beforeEach(() => {
setModel = jest.fn(); setModel = jest.fn();
dispose = jest.fn(); dispose = jest.fn();
modelsStorage = new Map();
modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel); modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel);
instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({ instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
setModel, setModel,
dispose, dispose,
onDidDispose: jest.fn(), onDidDispose: jest.fn(),
})); }));
jest.spyOn(monacoEditor, 'getModel').mockImplementation((uri) => {
return modelsStorage.get(uri.path);
});
}); });
it('throws an error if no dom element is supplied', () => { it('throws an error if no dom element is supplied', () => {
...@@ -72,6 +81,18 @@ describe('Base editor', () => { ...@@ -72,6 +81,18 @@ describe('Base editor', () => {
expect(setModel).toHaveBeenCalledWith(fakeModel); expect(setModel).toHaveBeenCalledWith(fakeModel);
}); });
it('does not create a new model if a model for the path already exists', () => {
modelSpy = jest
.spyOn(monacoEditor, 'createModel')
.mockImplementation((content, lang, uri) => modelsStorage.set(uri.path, content));
const instanceOptions = { el: editorEl, blobPath, blobContent, blobGlobalId: '' };
const a = editor.createInstance(instanceOptions);
const b = editor.createInstance(instanceOptions);
expect(a === b).toBe(false);
expect(modelSpy).toHaveBeenCalledTimes(1);
});
it('initializes the instance on a supplied DOM node', () => { it('initializes the instance on a supplied DOM node', () => {
editor.createInstance({ el: editorEl }); editor.createInstance({ el: editorEl });
...@@ -446,7 +467,7 @@ describe('Base editor', () => { ...@@ -446,7 +467,7 @@ describe('Base editor', () => {
expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation); expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation);
}); });
it('emits editor-ready event after all extensions were applied', async () => { it('emits EDITOR_READY_EVENT event after all extensions were applied', async () => {
const calls = []; const calls = [];
const eventSpy = jest.fn().mockImplementation(() => { const eventSpy = jest.fn().mockImplementation(() => {
calls.push('event'); calls.push('event');
...@@ -454,7 +475,7 @@ describe('Base editor', () => { ...@@ -454,7 +475,7 @@ describe('Base editor', () => {
const useSpy = jest.spyOn(editor, 'use').mockImplementation(() => { const useSpy = jest.spyOn(editor, 'use').mockImplementation(() => {
calls.push('use'); calls.push('use');
}); });
editorEl.addEventListener('editor-ready', eventSpy); editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
instance = instanceConstructor('foo, bar'); instance = instanceConstructor('foo, bar');
await waitForPromises(); await waitForPromises();
expect(useSpy.mock.calls).toHaveLength(2); expect(useSpy.mock.calls).toHaveLength(2);
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
mockProjectNamespace, mockProjectNamespace,
} from '../mock_data'; } from '../mock_data';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import TextEditor from '~/pipeline_editor/components/text_editor.vue'; import TextEditor from '~/pipeline_editor/components/text_editor.vue';
describe('~/pipeline_editor/components/text_editor.vue', () => { describe('~/pipeline_editor/components/text_editor.vue', () => {
...@@ -20,7 +21,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { ...@@ -20,7 +21,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
template: '<div/>', template: '<div/>',
props: ['value', 'fileName'], props: ['value', 'fileName'],
mounted() { mounted() {
this.$emit('editor-ready'); this.$emit(EDITOR_READY_EVENT);
}, },
methods: { methods: {
getEditor: () => ({ getEditor: () => ({
...@@ -44,7 +45,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { ...@@ -44,7 +45,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
value: mockCiYml, value: mockCiYml,
}, },
listeners: { listeners: {
'editor-ready': editorReadyListener, [EDITOR_READY_EVENT]: editorReadyListener,
}, },
stubs: { stubs: {
EditorLite: MockEditorLite, EditorLite: MockEditorLite,
...@@ -86,7 +87,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { ...@@ -86,7 +87,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
}); });
it('bubbles up events', () => { it('bubbles up events', () => {
findEditor().vm.$emit('editor-ready'); findEditor().vm.$emit(EDITOR_READY_EVENT);
expect(editorReadyListener).toHaveBeenCalled(); expect(editorReadyListener).toHaveBeenCalled();
}); });
......
...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue'; import EditorLite from '~/vue_shared/components/editor_lite.vue';
import Editor from '~/editor/editor_lite'; import Editor from '~/editor/editor_lite';
import { EDITOR_READY_EVENT } from '~/editor/constants';
jest.mock('~/editor/editor_lite'); jest.mock('~/editor/editor_lite');
...@@ -110,13 +111,13 @@ describe('Editor Lite component', () => { ...@@ -110,13 +111,13 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted().input).toEqual([[value]]); expect(wrapper.emitted().input).toEqual([[value]]);
}); });
it('emits editor-ready event when the Editor Lite is ready', async () => { it('emits EDITOR_READY_EVENT event when the Editor Lite is ready', async () => {
const el = wrapper.find({ ref: 'editor' }).element; const el = wrapper.find({ ref: 'editor' }).element;
expect(wrapper.emitted()['editor-ready']).toBeUndefined(); expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined();
await el.dispatchEvent(new Event('editor-ready')); await el.dispatchEvent(new Event(EDITOR_READY_EVENT));
expect(wrapper.emitted()['editor-ready']).toBeDefined(); expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeDefined();
}); });
it('component API `getEditor()` returns the editor instance', () => { it('component API `getEditor()` returns the editor instance', () => {
......
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