Merge branch '288317-extension-module' into 'master'
Source Editor Extension module See merge request gitlab-org/gitlab!73797
This commit is contained in:
commit
129e61735e
|
@ -1,15 +1,15 @@
|
||||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||||
import { __ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = __(
|
export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__(
|
||||||
'"el" parameter is required for createInstance()',
|
'SourceEditor|"el" parameter is required for createInstance()',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const URI_PREFIX = 'gitlab';
|
export const URI_PREFIX = 'gitlab';
|
||||||
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
|
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
|
||||||
|
|
||||||
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
|
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = s__(
|
||||||
'Source Editor instance is required to set up an extension.',
|
'SourceEditor|Source Editor instance is required to set up an extension.',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EDITOR_READY_EVENT = 'editor-ready';
|
export const EDITOR_READY_EVENT = 'editor-ready';
|
||||||
|
@ -20,6 +20,10 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
|
||||||
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
|
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
|
||||||
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
|
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
|
||||||
|
|
||||||
|
export const EDITOR_EXTENSION_DEFINITION_ERROR = s__(
|
||||||
|
'SourceEditor|Extension definition should be either a class or a function',
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// EXTENSIONS' CONSTANTS
|
// EXTENSIONS' CONSTANTS
|
||||||
//
|
//
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
// THIS IS AN EXAMPLE
|
||||||
|
//
|
||||||
|
// This file contains a basic documented example of the Source Editor extensions'
|
||||||
|
// API for your convenience. You can copy/paste it into your own file
|
||||||
|
// and adjust as you see fit
|
||||||
|
//
|
||||||
|
|
||||||
|
export class MyFancyExtension {
|
||||||
|
/**
|
||||||
|
* THE LIFE-CYCLE CALLBACKS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is called before the extension gets used by an instance,
|
||||||
|
* Use `onSetup` to setup Monaco directly:
|
||||||
|
* actions, keystrokes, update options, etc.
|
||||||
|
* Is called only once before the extension gets registered
|
||||||
|
*
|
||||||
|
* @param { Object } [setupOptions] The setupOptions object
|
||||||
|
* @param { Object } [instance] The Source Editor instance
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
||||||
|
onSetup(setupOptions, instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first thing called after the extension is
|
||||||
|
* registered and used by an instance.
|
||||||
|
* Is called every time the extension is applied
|
||||||
|
*
|
||||||
|
* @param { Object } [instance] The Source Editor instance
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
||||||
|
onUse(instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is called before un-using an extension. Can be used for time-critical
|
||||||
|
* actions like cleanup, reverting visual changes, and other user-facing
|
||||||
|
* updates.
|
||||||
|
*
|
||||||
|
* @param { Object } [instance] The Source Editor instance
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
||||||
|
onBeforeUnuse(instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is called right after an extension is removed from an instance (un-used)
|
||||||
|
* Can be used for non time-critical tasks like cleanup on the Monaco level
|
||||||
|
* (removing actions, keystrokes, etc.).
|
||||||
|
* onUnuse() will be executed during the browser's idle period
|
||||||
|
* (https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
|
||||||
|
*
|
||||||
|
* @param { Object } [instance] The Source Editor instance
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
||||||
|
onUnuse(instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public API of the extension: these are the methods that will be exposed
|
||||||
|
* to the end user
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
provides() {
|
||||||
|
return {
|
||||||
|
basic: () => {
|
||||||
|
// The most basic method not depending on anything
|
||||||
|
// Use: instance.basic();
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
return 'Foo Bar';
|
||||||
|
},
|
||||||
|
basicWithProp: () => {
|
||||||
|
// The methods with access to the props of the extension.
|
||||||
|
// The props can be either hardcoded (for example in `onSetup`), or
|
||||||
|
// can be dynamically passed as part of `setupOptions` object when
|
||||||
|
// using the extension.
|
||||||
|
// Use: instance.use({ definition: MyFancyExtension, setupOptions: { foo: 'bar' }});
|
||||||
|
return this.foo;
|
||||||
|
},
|
||||||
|
basicWithPropsAsList: (prop1, prop2) => {
|
||||||
|
// Just a simple method with local props
|
||||||
|
// The props are passed as usually.
|
||||||
|
// Use: instance.basicWithPropsAsList(prop1, prop2);
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
return `The prop1 is ${prop1}; the prop2 is ${prop2}`;
|
||||||
|
},
|
||||||
|
basicWithInstance: (instance) => {
|
||||||
|
// The method accessing the instance methods: either own or provided
|
||||||
|
// by previously-registered extensions
|
||||||
|
// `instance` is always supplied to all methods in provides() as THE LAST
|
||||||
|
// argument.
|
||||||
|
// You don't need to explicitly pass instance to this method:
|
||||||
|
// Use: instance.basicWithInstance();
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
return `We have access to the whole Instance! ${instance.alpha()}`;
|
||||||
|
},
|
||||||
|
advancedWithInstanceAndProps: ({ author, book } = {}, firstname, lastname, instance) => {
|
||||||
|
// Advanced method where
|
||||||
|
// { author, book } — are the props passed as an object
|
||||||
|
// prop1, prop2 — are the props passed as simple list
|
||||||
|
// instance — is automatically supplied, no need to pass it to
|
||||||
|
// the method explicitly
|
||||||
|
// Use: instance.advancedWithInstanceAndProps(
|
||||||
|
// {
|
||||||
|
// author: 'Franz Kafka',
|
||||||
|
// book: 'The Transformation'
|
||||||
|
// },
|
||||||
|
// 'Franz',
|
||||||
|
// 'Kafka'
|
||||||
|
// );
|
||||||
|
return `
|
||||||
|
The author is ${author}; the book is ${book}
|
||||||
|
The author's name is ${firstname}; the last name is ${lastname}
|
||||||
|
We have access to the whole Instance! For example, 'instance.alpha()': ${instance.alpha()}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
17
app/assets/javascripts/editor/source_editor_extension.js
Normal file
17
app/assets/javascripts/editor/source_editor_extension.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { EDITOR_EXTENSION_DEFINITION_ERROR } from './constants';
|
||||||
|
|
||||||
|
export default class EditorExtension {
|
||||||
|
constructor({ definition, setupOptions } = {}) {
|
||||||
|
if (typeof definition !== 'function') {
|
||||||
|
throw new Error(EDITOR_EXTENSION_DEFINITION_ERROR);
|
||||||
|
}
|
||||||
|
this.name = definition.name; // both class- and fn-based extensions have a name
|
||||||
|
this.setupOptions = setupOptions;
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
this.obj = new definition();
|
||||||
|
}
|
||||||
|
|
||||||
|
get api() {
|
||||||
|
return this.obj.provides();
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,9 +70,6 @@ msgstr ""
|
||||||
msgid "\"%{repository_name}\" size (%{repository_size}) is larger than the limit of %{limit}."
|
msgid "\"%{repository_name}\" size (%{repository_size}) is larger than the limit of %{limit}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "\"el\" parameter is required for createInstance()"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "#%{issueIid} (closed)"
|
msgid "#%{issueIid} (closed)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32577,9 +32574,6 @@ msgstr ""
|
||||||
msgid "Source Branch"
|
msgid "Source Branch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Source Editor instance is required to set up an extension."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source IP"
|
msgid "Source IP"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32598,6 +32592,15 @@ msgstr ""
|
||||||
msgid "Source project cannot be found."
|
msgid "Source project cannot be found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SourceEditor|\"el\" parameter is required for createInstance()"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SourceEditor|Extension definition should be either a class or a function"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SourceEditor|Source Editor instance is required to set up an extension."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Sourcegraph"
|
msgid "Sourcegraph"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
96
spec/frontend/editor/source_editor_extension_spec.js
Normal file
96
spec/frontend/editor/source_editor_extension_spec.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import EditorExtension from '~/editor/source_editor_extension';
|
||||||
|
import { EDITOR_EXTENSION_DEFINITION_ERROR } from '~/editor/constants';
|
||||||
|
|
||||||
|
class MyClassExtension {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
provides() {
|
||||||
|
return {
|
||||||
|
shared: () => 'extension',
|
||||||
|
classExtMethod: () => 'class own method',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MyFnExtension() {
|
||||||
|
return {
|
||||||
|
fnExtMethod: () => 'fn own method',
|
||||||
|
provides: () => {
|
||||||
|
return {
|
||||||
|
shared: () => 'extension',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyConstExt = () => {
|
||||||
|
return {
|
||||||
|
provides: () => {
|
||||||
|
return {
|
||||||
|
shared: () => 'extension',
|
||||||
|
constExtMethod: () => 'const own method',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Editor Extension', () => {
|
||||||
|
const dummyObj = { foo: 'bar' };
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
definition | setupOptions
|
||||||
|
${undefined} | ${undefined}
|
||||||
|
${undefined} | ${{}}
|
||||||
|
${undefined} | ${dummyObj}
|
||||||
|
${{}} | ${dummyObj}
|
||||||
|
${dummyObj} | ${dummyObj}
|
||||||
|
`(
|
||||||
|
'throws when definition = $definition and setupOptions = $setupOptions',
|
||||||
|
({ definition, setupOptions }) => {
|
||||||
|
const constructExtension = () => new EditorExtension({ definition, setupOptions });
|
||||||
|
expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
definition | setupOptions | expectedName
|
||||||
|
${MyClassExtension} | ${undefined} | ${'MyClassExtension'}
|
||||||
|
${MyClassExtension} | ${{}} | ${'MyClassExtension'}
|
||||||
|
${MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
|
||||||
|
${MyFnExtension} | ${undefined} | ${'MyFnExtension'}
|
||||||
|
${MyFnExtension} | ${{}} | ${'MyFnExtension'}
|
||||||
|
${MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
|
||||||
|
${MyConstExt} | ${undefined} | ${'MyConstExt'}
|
||||||
|
${MyConstExt} | ${{}} | ${'MyConstExt'}
|
||||||
|
${MyConstExt} | ${dummyObj} | ${'MyConstExt'}
|
||||||
|
`(
|
||||||
|
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
|
||||||
|
({ definition, setupOptions, expectedName }) => {
|
||||||
|
const extension = new EditorExtension({ definition, setupOptions });
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const constructedDefinition = new definition();
|
||||||
|
|
||||||
|
expect(extension).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: expectedName,
|
||||||
|
setupOptions,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(extension.obj.constructor.prototype).toBe(constructedDefinition.constructor.prototype);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('api', () => {
|
||||||
|
it.each`
|
||||||
|
definition | expectedKeys
|
||||||
|
${MyClassExtension} | ${['shared', 'classExtMethod']}
|
||||||
|
${MyFnExtension} | ${['shared']}
|
||||||
|
${MyConstExt} | ${['shared', 'constExtMethod']}
|
||||||
|
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
|
||||||
|
const extension = new EditorExtension({ definition });
|
||||||
|
const expectedApi = Object.fromEntries(
|
||||||
|
expectedKeys.map((key) => [key, expect.any(Function)]),
|
||||||
|
);
|
||||||
|
expect(extension.api).toEqual(expect.objectContaining(expectedApi));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue