Commit e3f1b92a authored by Simon Knox's avatar Simon Knox

Merge branch '292498/editor-lite-static-refactoring' into 'master'

Refactored Editor Lite functionality into static methods

See merge request gitlab-org/gitlab!52275
parents 916d3844 baea0902
......@@ -11,6 +11,8 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an extension.',
);
export const EDITOR_READY_EVENT = 'editor-ready';
//
// EXTENSIONS' CONSTANTS
//
......
......@@ -5,7 +5,7 @@ import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
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';
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.
*
......@@ -90,25 +132,15 @@ export default class EditorLite {
extensions = [],
...instanceOptions
} = {}) {
if (!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;
});
EditorLite.prepareInstance(el);
const instance = monacoEditor.create(el, {
...this.options,
...instanceOptions,
});
instance.setModel(model);
const model = EditorLite.createEditorModel({ blobGlobalId, blobPath, blobContent, instance });
instance.onDidDispose(() => {
const index = this.instances.findIndex((inst) => inst === instance);
this.instances.splice(index, 1);
......@@ -117,20 +149,7 @@ export default class EditorLite {
instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance);
instance.use = (args) => this.use(args, instance);
EditorLite.loadExtensions(extensions, instance)
.then((modules) => {
if (modules) {
modules.forEach((module) => {
instance.use(module.default);
});
}
})
.then(() => {
el.dispatchEvent(new Event('editor-ready'));
})
.catch((e) => {
throw e;
});
EditorLite.manageDefaultExtensions(instance, el, extensions);
this.instances.push(instance);
return instance;
......
<script>
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EDITOR_READY_EVENT } from '~/editor/constants';
export default {
components: {
......@@ -31,6 +32,7 @@ export default {
});
},
},
readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
......@@ -39,7 +41,7 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
@editor-ready="onEditorReady"
@[$options.readyEvent]="onEditorReady"
v-on="$listeners"
/>
</div>
......
<script>
import { debounce } from 'lodash';
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 }) {
const editor = new Editor({
......@@ -88,6 +88,7 @@ export default {
return this.editor;
},
},
readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
......@@ -95,7 +96,7 @@ export default {
:id="`editor-lite-${fileGlobalId}`"
ref="editor"
data-editor-loading
@editor-ready="$emit('editor-ready')"
@[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
......
......@@ -4,7 +4,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import Editor from '~/editor/editor_lite';
import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
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', () => {
let editorEl;
......@@ -43,16 +47,21 @@ describe('Base editor', () => {
let instanceSpy;
let setModel;
let dispose;
let modelsStorage;
beforeEach(() => {
setModel = jest.fn();
dispose = jest.fn();
modelsStorage = new Map();
modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel);
instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
setModel,
dispose,
onDidDispose: jest.fn(),
}));
jest.spyOn(monacoEditor, 'getModel').mockImplementation((uri) => {
return modelsStorage.get(uri.path);
});
});
it('throws an error if no dom element is supplied', () => {
......@@ -72,6 +81,18 @@ describe('Base editor', () => {
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', () => {
editor.createInstance({ el: editorEl });
......@@ -446,7 +467,7 @@ describe('Base editor', () => {
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 eventSpy = jest.fn().mockImplementation(() => {
calls.push('event');
......@@ -454,7 +475,7 @@ describe('Base editor', () => {
const useSpy = jest.spyOn(editor, 'use').mockImplementation(() => {
calls.push('use');
});
editorEl.addEventListener('editor-ready', eventSpy);
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
instance = instanceConstructor('foo, bar');
await waitForPromises();
expect(useSpy.mock.calls).toHaveLength(2);
......
......@@ -7,6 +7,7 @@ import {
mockProjectNamespace,
} from '../mock_data';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
describe('~/pipeline_editor/components/text_editor.vue', () => {
......@@ -20,7 +21,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
template: '<div/>',
props: ['value', 'fileName'],
mounted() {
this.$emit('editor-ready');
this.$emit(EDITOR_READY_EVENT);
},
methods: {
getEditor: () => ({
......@@ -44,7 +45,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
value: mockCiYml,
},
listeners: {
'editor-ready': editorReadyListener,
[EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
EditorLite: MockEditorLite,
......@@ -86,7 +87,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
});
it('bubbles up events', () => {
findEditor().vm.$emit('editor-ready');
findEditor().vm.$emit(EDITOR_READY_EVENT);
expect(editorReadyListener).toHaveBeenCalled();
});
......
......@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import Editor from '~/editor/editor_lite';
import { EDITOR_READY_EVENT } from '~/editor/constants';
jest.mock('~/editor/editor_lite');
......@@ -110,13 +111,13 @@ describe('Editor Lite component', () => {
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;
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', () => {
......
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