diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-15 18:15:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-15 18:15:54 +0300 |
commit | 231a6ae572807c481b71d906cad717fdffc85e0f (patch) | |
tree | 8b2415e1d4420bb3a5a14b365a37c8ef92bf6728 /app/assets/javascripts/editor | |
parent | 73b652cf4f890e91868055df8f76e6f869dd710d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/editor')
12 files changed, 655 insertions, 589 deletions
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index e855e304d27..2ae9c377683 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -42,6 +42,10 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__( // EXTENSIONS' CONSTANTS // +// Source Editor Base Extension +export const EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS = 'link-anchor'; +export const EXTENSION_BASE_LINE_NUMBERS_CLASS = 'line-numbers'; + // For CI config schemas the filename must match // '*.gitlab-ci.yml' regardless of project configuration. // https://gitlab.com/gitlab-org/gitlab/-/issues/293641 diff --git a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js index 33be6cf9e5d..52e2bb0b5ff 100644 --- a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js +++ b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js @@ -7,6 +7,16 @@ export class MyFancyExtension { /** + * A required getter returning the extension's name + * We have to provide it for every extension instead of relying on the built-in + * `name` prop because the prop does not survive the webpack's minification + * and the name mangling. + * @returns {string} + */ + static get extensionName() { + return 'MyFancyExtension'; + } + /** * THE LIFE-CYCLE CALLBACKS */ diff --git a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js index 7069568275d..0290bb84b5f 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js @@ -1,32 +1,27 @@ import ciSchemaPath from '~/editor/schema/ci.json'; import { registerSchema } from '~/ide/utils'; -import { SourceEditorExtension } from './source_editor_extension_base'; -export class CiSchemaExtension extends SourceEditorExtension { - /** - * Registers a syntax schema to the editor based on project - * identifier and commit. - * - * The schema is added to the file that is currently edited - * in the editor. - * - * @param {Object} opts - * @param {String} opts.projectNamespace - * @param {String} opts.projectPath - * @param {String?} opts.ref - Current ref. Defaults to main - */ - registerCiSchema() { - // In order for workers loaded from `data://` as the - // ones loaded by monaco editor, we use absolute URLs - // to fetch schema files, hence the `gon.gitlab_url` - // reference. This prevents error: - // "Failed to execute 'fetch' on 'WorkerGlobalScope'" - const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath; - const modelFileName = this.getModel().uri.path.split('/').pop(); +export class CiSchemaExtension { + static get extensionName() { + return 'CiSchema'; + } + // eslint-disable-next-line class-methods-use-this + provides() { + return { + registerCiSchema: (instance) => { + // In order for workers loaded from `data://` as the + // ones loaded by monaco editor, we use absolute URLs + // to fetch schema files, hence the `gon.gitlab_url` + // reference. This prevents error: + // "Failed to execute 'fetch' on 'WorkerGlobalScope'" + const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath; + const modelFileName = instance.getModel().uri.path.split('/').pop(); - registerSchema({ - uri: absoluteSchemaUrl, - fileMatch: [modelFileName], - }); + registerSchema({ + uri: absoluteSchemaUrl, + fileMatch: [modelFileName], + }); + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js index 03c68fed3b1..3aa19df964c 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js +++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js @@ -1,13 +1,16 @@ import { Range } from 'monaco-editor'; -import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; -import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION, EDITOR_TYPE_CODE } from '../constants'; +import { + EDITOR_TYPE_CODE, + EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS, + EXTENSION_BASE_LINE_NUMBERS_CLASS, +} from '../constants'; const hashRegexp = new RegExp('#?L', 'g'); const createAnchor = (href) => { const fragment = new DocumentFragment(); const el = document.createElement('a'); - el.classList.add('link-anchor'); + el.classList.add(EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS); el.href = href; fragment.appendChild(el); el.addEventListener('contextmenu', (e) => { @@ -17,38 +20,46 @@ const createAnchor = (href) => { }; export class SourceEditorExtension { - constructor({ instance, ...options } = {}) { - if (instance) { - Object.assign(instance, options); - SourceEditorExtension.highlightLines(instance); - if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) { - SourceEditorExtension.setupLineLinking(instance); - } - SourceEditorExtension.deferRerender(instance); - } else if (Object.entries(options).length) { - throw new Error(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION); + static get extensionName() { + return 'BaseExtension'; + } + + // eslint-disable-next-line class-methods-use-this + onUse(instance) { + SourceEditorExtension.highlightLines(instance); + if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) { + SourceEditorExtension.setupLineLinking(instance); } } - static deferRerender(instance) { - waitForCSSLoaded(() => { - instance.layout(); - }); + static onMouseMoveHandler(e) { + const target = e.target.element; + if (target.classList.contains(EXTENSION_BASE_LINE_NUMBERS_CLASS)) { + const lineNum = e.target.position.lineNumber; + const hrefAttr = `#L${lineNum}`; + let lineLink = target.querySelector('a'); + if (!lineLink) { + lineLink = createAnchor(hrefAttr); + target.appendChild(lineLink); + } + } } - static removeHighlights(instance) { - Object.assign(instance, { - lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []), + static setupLineLinking(instance) { + instance.onMouseMove(SourceEditorExtension.onMouseMoveHandler); + instance.onMouseDown((e) => { + const isCorrectAnchor = e.target.element.classList.contains( + EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS, + ); + if (!isCorrectAnchor) { + return; + } + if (instance.lineDecorations) { + instance.deltaDecorations(instance.lineDecorations, []); + } }); } - /** - * Returns a function that can only be invoked once between - * each browser screen repaint. - * @param {Object} instance - The Source Editor instance - * @param {Array} bounds - The [start, end] array with start - * and end coordinates for highlighting - */ static highlightLines(instance, bounds = null) { const [start, end] = bounds && Array.isArray(bounds) @@ -74,29 +85,29 @@ export class SourceEditorExtension { } } - static onMouseMoveHandler(e) { - const target = e.target.element; - if (target.classList.contains('line-numbers')) { - const lineNum = e.target.position.lineNumber; - const hrefAttr = `#L${lineNum}`; - let el = target.querySelector('a'); - if (!el) { - el = createAnchor(hrefAttr); - target.appendChild(el); - } - } - } + // eslint-disable-next-line class-methods-use-this + provides() { + return { + /** + * Removes existing line decorations and updates the reference on the instance + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + */ + removeHighlights: (instance) => { + Object.assign(instance, { + lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []), + }); + }, - static setupLineLinking(instance) { - instance.onMouseMove(SourceEditorExtension.onMouseMoveHandler); - instance.onMouseDown((e) => { - const isCorrectAnchor = e.target.element.classList.contains('link-anchor'); - if (!isCorrectAnchor) { - return; - } - if (instance.lineDecorations) { - instance.deltaDecorations(instance.lineDecorations, []); - } - }); + /** + * Returns a function that can only be invoked once between + * each browser screen repaint. + * @param {Array} bounds - The [start, end] array with start + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * and end coordinates for highlighting + */ + highlightLines(instance, bounds = null) { + SourceEditorExtension.highlightLines(instance, bounds); + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js index 397e090ed30..ba4980896e5 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js @@ -1,8 +1,16 @@ import { Position } from 'monaco-editor'; -import { SourceEditorExtension } from './source_editor_extension_base'; -export class FileTemplateExtension extends SourceEditorExtension { - navigateFileStart() { - this.setPosition(new Position(1, 1)); +export class FileTemplateExtension { + static get extensionName() { + return 'FileTemplate'; + } + + // eslint-disable-next-line class-methods-use-this + provides() { + return { + navigateFileStart: (instance) => { + instance.setPosition(new Position(1, 1)); + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js index bec7fe7e25f..a16fe93026e 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js @@ -1,97 +1,102 @@ -import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; - -export class EditorMarkdownExtension extends EditorMarkdownPreviewExtension { - getSelectedText(selection = this.getSelection()) { - const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; - const valArray = this.getValue().split('\n'); - let text = ''; - if (startLineNumber === endLineNumber) { - text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1); - } else { - const startLineText = valArray[startLineNumber - 1].slice(startColumn - 1); - const endLineText = valArray[endLineNumber - 1].slice(0, endColumn - 1); - - for (let i = startLineNumber, k = endLineNumber - 1; i < k; i += 1) { - text += `${valArray[i]}`; - if (i !== k - 1) text += `\n`; - } - text = text - ? [startLineText, text, endLineText].join('\n') - : [startLineText, endLineText].join('\n'); - } - return text; +export class EditorMarkdownExtension { + static get extensionName() { + return 'EditorMarkdown'; } - replaceSelectedText(text, select = undefined) { - const forceMoveMarkers = !select; - this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]); - } - - moveCursor(dx = 0, dy = 0) { - const pos = this.getPosition(); - pos.column += dx; - pos.lineNumber += dy; - this.setPosition(pos); - } + // eslint-disable-next-line class-methods-use-this + provides() { + return { + getSelectedText: (instance, selection = instance.getSelection()) => { + const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; + const valArray = instance.getValue().split('\n'); + let text = ''; + if (startLineNumber === endLineNumber) { + text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1); + } else { + const startLineText = valArray[startLineNumber - 1].slice(startColumn - 1); + const endLineText = valArray[endLineNumber - 1].slice(0, endColumn - 1); - /** - * Adjust existing selection to select text within the original selection. - * - If `selectedText` is not supplied, we fetch selected text with - * - * ALGORITHM: - * - * MULTI-LINE SELECTION - * 1. Find line that contains `toSelect` text. - * 2. Using the index of this line and the position of `toSelect` text in it, - * construct: - * * newStartLineNumber - * * newStartColumn - * - * SINGLE-LINE SELECTION - * 1. Use `startLineNumber` from the current selection as `newStartLineNumber` - * 2. Find the position of `toSelect` text in it to get `newStartColumn` - * - * 3. `newEndLineNumber` — Since this method is supposed to be used with - * markdown decorators that are pretty short, the `newEndLineNumber` is - * suggested to be assumed the same as the startLine. - * 4. `newEndColumn` — pretty obvious - * 5. Adjust the start and end positions of the current selection - * 6. Re-set selection on the instance - * - * @param {string} toSelect - New text to select within current selection. - * @param {string} selectedText - Currently selected text. It's just a - * shortcut: If it's not supplied, we fetch selected text from the instance - */ - selectWithinSelection(toSelect, selectedText) { - const currentSelection = this.getSelection(); - if (currentSelection.isEmpty() || !toSelect) { - return; - } - const text = selectedText || this.getSelectedText(currentSelection); - let lineShift; - let newStartLineNumber; - let newStartColumn; + for (let i = startLineNumber, k = endLineNumber - 1; i < k; i += 1) { + text += `${valArray[i]}`; + if (i !== k - 1) text += `\n`; + } + text = text + ? [startLineText, text, endLineText].join('\n') + : [startLineText, endLineText].join('\n'); + } + return text; + }, + replaceSelectedText: (instance, text, select) => { + const forceMoveMarkers = !select; + instance.executeEdits('', [{ range: instance.getSelection(), text, forceMoveMarkers }]); + }, + moveCursor: (instance, dx = 0, dy = 0) => { + const pos = instance.getPosition(); + pos.column += dx; + pos.lineNumber += dy; + instance.setPosition(pos); + }, + /** + * Adjust existing selection to select text within the original selection. + * - If `selectedText` is not supplied, we fetch selected text with + * + * ALGORITHM: + * + * MULTI-LINE SELECTION + * 1. Find line that contains `toSelect` text. + * 2. Using the index of this line and the position of `toSelect` text in it, + * construct: + * * newStartLineNumber + * * newStartColumn + * + * SINGLE-LINE SELECTION + * 1. Use `startLineNumber` from the current selection as `newStartLineNumber` + * 2. Find the position of `toSelect` text in it to get `newStartColumn` + * + * 3. `newEndLineNumber` — Since this method is supposed to be used with + * markdown decorators that are pretty short, the `newEndLineNumber` is + * suggested to be assumed the same as the startLine. + * 4. `newEndColumn` — pretty obvious + * 5. Adjust the start and end positions of the current selection + * 6. Re-set selection on the instance + * + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance. Is passed automatically. + * @param {string} toSelect - New text to select within current selection. + * @param {string} selectedText - Currently selected text. It's just a + * shortcut: If it's not supplied, we fetch selected text from the instance + */ + selectWithinSelection: (instance, toSelect, selectedText) => { + const currentSelection = instance.getSelection(); + if (currentSelection.isEmpty() || !toSelect) { + return; + } + const text = selectedText || instance.getSelectedText(currentSelection); + let lineShift; + let newStartLineNumber; + let newStartColumn; - const textLines = text.split('\n'); + const textLines = text.split('\n'); - if (textLines.length > 1) { - // Multi-line selection - lineShift = textLines.findIndex((line) => line.indexOf(toSelect) !== -1); - newStartLineNumber = currentSelection.startLineNumber + lineShift; - newStartColumn = textLines[lineShift].indexOf(toSelect) + 1; - } else { - // Single-line selection - newStartLineNumber = currentSelection.startLineNumber; - newStartColumn = currentSelection.startColumn + text.indexOf(toSelect); - } + if (textLines.length > 1) { + // Multi-line selection + lineShift = textLines.findIndex((line) => line.indexOf(toSelect) !== -1); + newStartLineNumber = currentSelection.startLineNumber + lineShift; + newStartColumn = textLines[lineShift].indexOf(toSelect) + 1; + } else { + // Single-line selection + newStartLineNumber = currentSelection.startLineNumber; + newStartColumn = currentSelection.startColumn + text.indexOf(toSelect); + } - const newEndLineNumber = newStartLineNumber; - const newEndColumn = newStartColumn + toSelect.length; + const newEndLineNumber = newStartLineNumber; + const newEndColumn = newStartColumn + toSelect.length; - const newSelection = currentSelection - .setStartPosition(newStartLineNumber, newStartColumn) - .setEndPosition(newEndLineNumber, newEndColumn); + const newSelection = currentSelection + .setStartPosition(newStartLineNumber, newStartColumn) + .setEndPosition(newEndLineNumber, newEndColumn); - this.setSelection(newSelection); + instance.setSelection(newSelection); + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js index 526de7f8932..9d53268c340 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js @@ -12,9 +12,8 @@ import { EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS, EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, } from '../constants'; -import { SourceEditorExtension } from './source_editor_extension_base'; -const getPreview = (text, previewMarkdownPath) => { +const fetchPreview = (text, previewMarkdownPath) => { return axios .post(previewMarkdownPath, { text, @@ -34,19 +33,20 @@ const setupDomElement = ({ injectToEl = null } = {}) => { return previewEl; }; -export class EditorMarkdownPreviewExtension extends SourceEditorExtension { - constructor({ instance, previewMarkdownPath, ...args } = {}) { - super({ instance, ...args }); - Object.assign(instance, { - previewMarkdownPath, - preview: { - el: undefined, - action: undefined, - shown: false, - modelChangeListener: undefined, - }, - }); - this.setupPreviewAction.call(instance); +export class EditorMarkdownPreviewExtension { + static get extensionName() { + return 'EditorMarkdownPreview'; + } + + onSetup(instance, setupOptions) { + this.preview = { + el: undefined, + action: undefined, + shown: false, + modelChangeListener: undefined, + path: setupOptions.previewMarkdownPath, + }; + this.setupPreviewAction(instance); instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => { if (newLanguage === 'markdown' && oldLanguage !== newLanguage) { @@ -68,43 +68,31 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension { }); } - static togglePreviewLayout() { - const { width, height } = this.getLayoutInfo(); + togglePreviewLayout(instance) { + const { width, height } = instance.getLayoutInfo(); const newWidth = this.preview.shown ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH; - this.layout({ width: newWidth, height }); + instance.layout({ width: newWidth, height }); } - static togglePreviewPanel() { - const parentEl = this.getDomNode().parentElement; + togglePreviewPanel(instance) { + const parentEl = instance.getDomNode().parentElement; const { el: previewEl } = this.preview; parentEl.classList.toggle(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS); if (previewEl.style.display === 'none') { // Show the preview panel - this.fetchPreview(); + this.fetchPreview(instance); } else { // Hide the preview panel previewEl.style.display = 'none'; } } - cleanup() { - if (this.preview.modelChangeListener) { - this.preview.modelChangeListener.dispose(); - } - this.preview.action.dispose(); - if (this.preview.shown) { - EditorMarkdownPreviewExtension.togglePreviewPanel.call(this); - EditorMarkdownPreviewExtension.togglePreviewLayout.call(this); - } - this.preview.shown = false; - } - - fetchPreview() { + fetchPreview(instance) { const { el: previewEl } = this.preview; - getPreview(this.getValue(), this.previewMarkdownPath) + fetchPreview(instance.getValue(), this.preview.path) .then((data) => { previewEl.innerHTML = sanitize(data); syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight')); @@ -113,10 +101,10 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension { .catch(() => createFlash(BLOB_PREVIEW_ERROR)); } - setupPreviewAction() { - if (this.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return; + setupPreviewAction(instance) { + if (instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return; - this.preview.action = this.addAction({ + this.preview.action = instance.addAction({ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, label: __('Preview Markdown'), keybindings: [ @@ -128,27 +116,52 @@ export class EditorMarkdownPreviewExtension extends SourceEditorExtension { // Method that will be executed when the action is triggered. // @param ed The editor instance is passed in as a convenience - run(instance) { - instance.togglePreview(); + run(inst) { + inst.togglePreview(); }, }); } - togglePreview() { - if (!this.preview?.el) { - this.preview.el = setupDomElement({ injectToEl: this.getDomNode().parentElement }); - } - EditorMarkdownPreviewExtension.togglePreviewLayout.call(this); - EditorMarkdownPreviewExtension.togglePreviewPanel.call(this); + provides() { + return { + markdownPreview: this.preview, - if (!this.preview?.shown) { - this.preview.modelChangeListener = this.onDidChangeModelContent( - debounce(this.fetchPreview.bind(this), EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY), - ); - } else { - this.preview.modelChangeListener.dispose(); - } + cleanup: (instance) => { + if (this.preview.modelChangeListener) { + this.preview.modelChangeListener.dispose(); + } + this.preview.action.dispose(); + if (this.preview.shown) { + this.togglePreviewPanel(instance); + this.togglePreviewLayout(instance); + } + this.preview.shown = false; + }, + + fetchPreview: (instance) => this.fetchPreview(instance), - this.preview.shown = !this.preview?.shown; + setupPreviewAction: (instance) => this.setupPreviewAction(instance), + + togglePreview: (instance) => { + if (!this.preview?.el) { + this.preview.el = setupDomElement({ injectToEl: instance.getDomNode().parentElement }); + } + this.togglePreviewLayout(instance); + this.togglePreviewPanel(instance); + + if (!this.preview?.shown) { + this.preview.modelChangeListener = instance.onDidChangeModelContent( + debounce( + this.fetchPreview.bind(this, instance), + EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, + ), + ); + } else { + this.preview.modelChangeListener.dispose(); + } + + this.preview.shown = !this.preview?.shown; + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js index 98e05489c1c..4e8c11bac54 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js @@ -1,7 +1,15 @@ +/** + * A WebIDE Extension options for Source Editor + * @typedef {Object} WebIDEExtensionOptions + * @property {Object} modelManager The root manager for WebIDE models + * @property {Object} store The state store for communication + * @property {Object} file + * @property {Object} options The Monaco editor options + */ + import { debounce } from 'lodash'; import { KeyCode, KeyMod, Range } from 'monaco-editor'; import { EDITOR_TYPE_DIFF } from '~/editor/constants'; -import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import Disposable from '~/ide/lib/common/disposable'; import { editorOptions } from '~/ide/lib/editor_options'; import keymap from '~/ide/lib/keymap.json'; @@ -11,154 +19,168 @@ const isDiffEditorType = (instance) => { }; export const UPDATE_DIMENSIONS_DELAY = 200; +const defaultOptions = { + modelManager: undefined, + store: undefined, + file: undefined, + options: {}, +}; -export class EditorWebIdeExtension extends SourceEditorExtension { - constructor({ instance, modelManager, ...options } = {}) { - super({ - instance, - ...options, - modelManager, - disposable: new Disposable(), - debouncedUpdate: debounce(() => { - instance.updateDimensions(); - }, UPDATE_DIMENSIONS_DELAY), - }); - - window.addEventListener('resize', instance.debouncedUpdate, false); - - instance.onDidDispose(() => { - window.removeEventListener('resize', instance.debouncedUpdate); - - // catch any potential errors with disposing the error - // this is mainly for tests caused by elements not existing - try { - instance.disposable.dispose(); - } catch (e) { - if (process.env.NODE_ENV !== 'test') { - // eslint-disable-next-line no-console - console.error(e); - } - } - }); +const addActions = (instance, store) => { + const getKeyCode = (key) => { + const monacoKeyMod = key.indexOf('KEY_') === 0; - EditorWebIdeExtension.addActions(instance); - } + return monacoKeyMod ? KeyCode[key] : KeyMod[key]; + }; - static addActions(instance) { - const { store } = instance; - const getKeyCode = (key) => { - const monacoKeyMod = key.indexOf('KEY_') === 0; + keymap.forEach((command) => { + const { bindings, id, label, action } = command; - return monacoKeyMod ? KeyCode[key] : KeyMod[key]; - }; + const keybindings = bindings.map((binding) => { + const keys = binding.split('+'); - keymap.forEach((command) => { - const { bindings, id, label, action } = command; - - const keybindings = bindings.map((binding) => { - const keys = binding.split('+'); - - // eslint-disable-next-line no-bitwise - return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); - }); - - instance.addAction({ - id, - label, - keybindings, - run() { - store.dispatch(action.name, action.params); - return null; - }, - }); + // eslint-disable-next-line no-bitwise + return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); }); - } - - createModel(file, head = null) { - return this.modelManager.addModel(file, head); - } - - attachModel(model) { - if (isDiffEditorType(this)) { - this.setModel({ - original: model.getOriginalModel(), - modified: model.getModel(), - }); - return; - } - - this.setModel(model.getModel()); + instance.addAction({ + id, + label, + keybindings, + run() { + store.dispatch(action.name, action.params); + return null; + }, + }); + }); +}; - this.updateOptions( - editorOptions.reduce((acc, obj) => { - Object.keys(obj).forEach((key) => { - Object.assign(acc, { - [key]: obj[key](model), - }); - }); - return acc; - }, {}), - ); - } +const renderSideBySide = (domElement) => { + return domElement.offsetWidth >= 700; +}; - attachMergeRequestModel(model) { - this.setModel({ - original: model.getBaseModel(), - modified: model.getModel(), +const updateInstanceDimensions = (instance) => { + instance.layout(); + if (isDiffEditorType(instance)) { + instance.updateOptions({ + renderSideBySide: renderSideBySide(instance.getDomNode()), }); } +}; - updateDimensions() { - this.layout(); - this.updateDiffView(); +export class EditorWebIdeExtension { + static get extensionName() { + return 'EditorWebIde'; } - setPos({ lineNumber, column }) { - this.revealPositionInCenter({ - lineNumber, - column, - }); - this.setPosition({ - lineNumber, - column, - }); + /** + * Set up the WebIDE extension for Source Editor + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param {WebIDEExtensionOptions} setupOptions + */ + onSetup(instance, setupOptions = defaultOptions) { + this.modelManager = setupOptions.modelManager; + this.store = setupOptions.store; + this.file = setupOptions.file; + this.options = setupOptions.options; + + this.disposable = new Disposable(); + this.debouncedUpdate = debounce(() => { + updateInstanceDimensions(instance); + }, UPDATE_DIMENSIONS_DELAY); + + addActions(instance, setupOptions.store); } - onPositionChange(cb) { - if (!this.onDidChangeCursorPosition) { - return; - } + onUse(instance) { + window.addEventListener('resize', this.debouncedUpdate, false); - this.disposable.add(this.onDidChangeCursorPosition((e) => cb(this, e))); + instance.onDidDispose(() => { + this.onUnuse(); + }); } - updateDiffView() { - if (!isDiffEditorType(this)) { - return; + onUnuse() { + window.removeEventListener('resize', this.debouncedUpdate); + + // catch any potential errors with disposing the error + // this is mainly for tests caused by elements not existing + try { + this.disposable.dispose(); + } catch (e) { + if (process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console + console.error(e); + } } - - this.updateOptions({ - renderSideBySide: EditorWebIdeExtension.renderSideBySide(this.getDomNode()), - }); } - replaceSelectedText(text) { - let selection = this.getSelection(); - const range = new Range( - selection.startLineNumber, - selection.startColumn, - selection.endLineNumber, - selection.endColumn, - ); + provides() { + return { + createModel: (instance, file, head = null) => { + return this.modelManager.addModel(file, head); + }, + attachModel: (instance, model) => { + if (isDiffEditorType(instance)) { + instance.setModel({ + original: model.getOriginalModel(), + modified: model.getModel(), + }); - this.executeEdits('', [{ range, text }]); + return; + } - selection = this.getSelection(); - this.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn }); - } + instance.setModel(model.getModel()); + + instance.updateOptions( + editorOptions.reduce((acc, obj) => { + Object.keys(obj).forEach((key) => { + Object.assign(acc, { + [key]: obj[key](model), + }); + }); + return acc; + }, {}), + ); + }, + attachMergeRequestModel: (instance, model) => { + instance.setModel({ + original: model.getBaseModel(), + modified: model.getModel(), + }); + }, + updateDimensions: (instance) => updateInstanceDimensions(instance), + setPos: (instance, { lineNumber, column }) => { + instance.revealPositionInCenter({ + lineNumber, + column, + }); + instance.setPosition({ + lineNumber, + column, + }); + }, + onPositionChange: (instance, cb) => { + if (typeof instance.onDidChangeCursorPosition !== 'function') { + return; + } - static renderSideBySide(domElement) { - return domElement.offsetWidth >= 700; + this.disposable.add(instance.onDidChangeCursorPosition((e) => cb(instance, e))); + }, + replaceSelectedText: (instance, text) => { + let selection = instance.getSelection(); + const range = new Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn, + ); + + instance.executeEdits('', [{ range, text }]); + + selection = instance.getSelection(); + instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn }); + }, + }; } } diff --git a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js index 212e09c8724..05ce617ca7c 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js @@ -1,50 +1,46 @@ +/** + * A Yaml Editor Extension options for Source Editor + * @typedef {Object} YamlEditorExtensionOptions + * @property { boolean } enableComments Convert model nodes with the comment + * pattern to comments? + * @property { string } highlightPath Add a line highlight to the + * node specified by this e.g. `"foo.bar[0]"` + * @property { * } model Any JS Object that will be stringified and used as the + * editor's value. Equivalent to using `setDataModel()` + * @property options SourceEditorExtension Options + */ + import { toPath } from 'lodash'; import { parseDocument, Document, visit, isScalar, isCollection, isMap } from 'yaml'; import { findPair } from 'yaml/util'; -import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; -export class YamlEditorExtension extends SourceEditorExtension { +export class YamlEditorExtension { + static get extensionName() { + return 'YamlEditor'; + } + /** * Extends the source editor with capabilities for yaml files. * - * @param { Instance } instance Source Editor Instance - * @param { boolean } enableComments Convert model nodes with the comment - * pattern to comments? - * @param { string } highlightPath Add a line highlight to the - * node specified by this e.g. `"foo.bar[0]"` - * @param { * } model Any JS Object that will be stringified and used as the - * editor's value. Equivalent to using `setDataModel()` - * @param options SourceEditorExtension Options + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param {YamlEditorExtensionOptions} setupOptions */ - constructor({ - instance, - enableComments = false, - highlightPath = null, - model = null, - ...options - } = {}) { - super({ - instance, - options: { - ...options, - enableComments, - highlightPath, - }, - }); + onSetup(instance, setupOptions = {}) { + const { enableComments = false, highlightPath = null, model = null } = setupOptions; + this.enableComments = enableComments; + this.highlightPath = highlightPath; + this.model = model; if (model) { - YamlEditorExtension.initFromModel(instance, model); + this.initFromModel(instance, model); } instance.onDidChangeModelContent(() => instance.onUpdate()); } - /** - * @private - */ - static initFromModel(instance, model) { + initFromModel(instance, model) { const doc = new Document(model); - if (instance.options.enableComments) { + if (this.enableComments) { YamlEditorExtension.transformComments(doc); } instance.setValue(doc.toString()); @@ -160,110 +156,13 @@ export class YamlEditorExtension extends SourceEditorExtension { return doc; } - /** - * Get the editor's value parsed as a `Document` as defined by the `yaml` - * package - * @returns {Document} - */ - getDoc() { - return parseDocument(this.getValue()); - } - - /** - * Accepts a `Document` as defined by the `yaml` package and - * sets the Editor's value to a stringified version of it. - * @param { Document } doc - */ - setDoc(doc) { - if (this.options.enableComments) { - YamlEditorExtension.transformComments(doc); - } - - if (!this.getValue()) { - this.setValue(doc.toString()); - } else { - this.updateValue(doc.toString()); - } - } - - /** - * Returns the parsed value of the Editor's content as JS. - * @returns {*} - */ - getDataModel() { - return this.getDoc().toJS(); - } - - /** - * Accepts any JS Object and sets the Editor's value to a stringified version - * of that value. - * - * @param value - */ - setDataModel(value) { - this.setDoc(new Document(value)); - } - - /** - * Method to be executed when the Editor's <TextModel> was updated - */ - onUpdate() { - if (this.options.highlightPath) { - this.highlight(this.options.highlightPath); - } - } - - /** - * Set the editors content to the input without recreating the content model. - * - * @param blob - */ - updateValue(blob) { - // Using applyEdits() instead of setValue() ensures that tokens such as - // highlighted lines aren't deleted/recreated which causes a flicker. - const model = this.getModel(); - model.applyEdits([ - { - // A nice improvement would be to replace getFullModelRange() with - // a range of the actual diff, avoiding re-formatting the document, - // but that's something for a later iteration. - range: model.getFullModelRange(), - text: blob, - }, - ]); - } - - /** - * Add a line highlight style to the node specified by the path. - * - * @param {string|null|false} path A path to a node of the Editor's value, - * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all - * highlights. - */ - highlight(path) { - if (this.options.highlightPath === path) return; - if (!path) { - SourceEditorExtension.removeHighlights(this); - } else { - const res = this.locate(path); - SourceEditorExtension.highlightLines(this, res); - } - this.options.highlightPath = path || null; + static getDoc(instance) { + return parseDocument(instance.getValue()); } - /** - * Return the line numbers of a certain node identified by `path` within - * the yaml. - * - * @param {string} path A path to a node, eg. `foo.bar[0]` - * @returns {number[]} Array following the schema `[firstLine, lastLine]` - * (both inclusive) - * - * @throws {Error} Will throw if the path is not found inside the document - */ - locate(path) { + static locate(instance, path) { if (!path) throw Error(`No path provided.`); - const blob = this.getValue(); + const blob = instance.getValue(); const doc = parseDocument(blob); const pathArray = toPath(path); @@ -290,4 +189,120 @@ export class YamlEditorExtension extends SourceEditorExtension { const endLine = (endSlice.match(/\n/g) || []).length; return [startLine, endLine]; } + + setDoc(instance, doc) { + if (this.enableComments) { + YamlEditorExtension.transformComments(doc); + } + + if (!instance.getValue()) { + instance.setValue(doc.toString()); + } else { + instance.updateValue(doc.toString()); + } + } + + highlight(instance, path) { + // IMPORTANT + // removeHighlight and highlightLines both come from + // SourceEditorExtension. So it has to be installed prior to this extension + if (this.highlightPath === path) return; + if (!path) { + instance.removeHighlights(); + } else { + const res = YamlEditorExtension.locate(instance, path); + instance.highlightLines(res); + } + this.highlightPath = path || null; + } + + provides() { + return { + /** + * Get the editor's value parsed as a `Document` as defined by the `yaml` + * package + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @returns {Document} + */ + getDoc: (instance) => YamlEditorExtension.getDoc(instance), + + /** + * Accepts a `Document` as defined by the `yaml` package and + * sets the Editor's value to a stringified version of it. + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param { Document } doc + */ + setDoc: (instance, doc) => this.setDoc(instance, doc), + + /** + * Returns the parsed value of the Editor's content as JS. + * @returns {*} + */ + getDataModel: (instance) => YamlEditorExtension.getDoc(instance).toJS(), + + /** + * Accepts any JS Object and sets the Editor's value to a stringified version + * of that value. + * + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param value + */ + setDataModel: (instance, value) => this.setDoc(instance, new Document(value)), + + /** + * Method to be executed when the Editor's <TextModel> was updated + */ + onUpdate: (instance) => { + if (this.highlightPath) { + this.highlight(instance, this.highlightPath); + } + }, + + /** + * Set the editors content to the input without recreating the content model. + * + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param blob + */ + updateValue: (instance, blob) => { + // Using applyEdits() instead of setValue() ensures that tokens such as + // highlighted lines aren't deleted/recreated which causes a flicker. + const model = instance.getModel(); + model.applyEdits([ + { + // A nice improvement would be to replace getFullModelRange() with + // a range of the actual diff, avoiding re-formatting the document, + // but that's something for a later iteration. + range: model.getFullModelRange(), + text: blob, + }, + ]); + }, + + /** + * Add a line highlight style to the node specified by the path. + * + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param {string|null|false} path A path to a node of the Editor's value, + * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all + * highlights. + */ + highlight: (instance, path) => this.highlight(instance, path), + + /** + * Return the line numbers of a certain node identified by `path` within + * the yaml. + * + * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance + * @param {string} path A path to a node, eg. `foo.bar[0]` + * @returns {number[]} Array following the schema `[firstLine, lastLine]` + * (both inclusive) + * + * @throws {Error} Will throw if the path is not found inside the document + */ + locate: (instance, path) => YamlEditorExtension.locate(instance, path), + + initFromModel: (instance, model) => this.initFromModel(instance, model), + }; + } } diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js index 26fbd1f4d8a..57e2b0da565 100644 --- a/app/assets/javascripts/editor/source_editor.js +++ b/app/assets/javascripts/editor/source_editor.js @@ -1,4 +1,5 @@ import { editor as monacoEditor, Uri } from 'monaco-editor'; +import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import languages from '~/ide/lib/languages'; import { registerLanguages } from '~/ide/utils'; @@ -11,10 +12,39 @@ import { EDITOR_TYPE_DIFF, } from './constants'; import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils'; +import EditorInstance from './source_editor_instance'; + +const instanceRemoveFromRegistry = (editor, instance) => { + const index = editor.instances.findIndex((inst) => inst === instance); + editor.instances.splice(index, 1); +}; + +const instanceDisposeModels = (editor, instance, model) => { + const instanceModel = instance.getModel() || model; + if (!instanceModel) { + return; + } + if (instance.getEditorType() === EDITOR_TYPE_DIFF) { + const { original, modified } = instanceModel; + if (original) { + original.dispose(); + } + if (modified) { + modified.dispose(); + } + } else { + instanceModel.dispose(); + } +}; export default class SourceEditor { + /** + * Constructs a global editor. + * @param {Object} options - Monaco config options used to create the editor + */ constructor(options = {}) { this.instances = []; + this.extensionsStore = new Map(); this.options = { extraEditorClassName: 'gl-source-editor', ...defaultEditorOptions, @@ -26,19 +56,6 @@ export default class SourceEditor { registerLanguages(...languages); } - static mixIntoInstance(source, inst) { - if (!inst) { - return; - } - const isClassInstance = source.constructor.prototype !== Object.prototype; - const sanitizedSource = isClassInstance ? source.constructor.prototype : source; - Object.getOwnPropertyNames(sanitizedSource).forEach((prop) => { - if (prop !== 'constructor') { - Object.assign(inst, { [prop]: source[prop] }); - } - }); - } - static prepareInstance(el) { if (!el) { throw new Error(SOURCE_EDITOR_INSTANCE_ERROR_NO_EL); @@ -78,71 +95,17 @@ export default class SourceEditor { return diffModel; } - static convertMonacoToELInstance = (inst) => { - const sourceEditorInstanceAPI = { - updateModelLanguage: (path) => { - return SourceEditor.instanceUpdateLanguage(inst, path); - }, - use: (exts = []) => { - return SourceEditor.instanceApplyExtension(inst, exts); - }, - }; - const handler = { - get(target, prop, receiver) { - if (Reflect.has(sourceEditorInstanceAPI, prop)) { - return sourceEditorInstanceAPI[prop]; - } - return Reflect.get(target, prop, receiver); - }, - }; - return new Proxy(inst, handler); - }; - - static instanceUpdateLanguage(inst, path) { - const lang = getBlobLanguage(path); - const model = inst.getModel(); - return monacoEditor.setModelLanguage(model, lang); - } - - static instanceApplyExtension(inst, exts = []) { - const extensions = [].concat(exts); - extensions.forEach((extension) => { - SourceEditor.mixIntoInstance(extension, inst); - }); - return inst; - } - - static instanceRemoveFromRegistry(editor, instance) { - const index = editor.instances.findIndex((inst) => inst === instance); - editor.instances.splice(index, 1); - } - - static instanceDisposeModels(editor, instance, model) { - const instanceModel = instance.getModel() || model; - if (!instanceModel) { - return; - } - if (instance.getEditorType() === EDITOR_TYPE_DIFF) { - const { original, modified } = instanceModel; - if (original) { - original.dispose(); - } - if (modified) { - modified.dispose(); - } - } else { - instanceModel.dispose(); - } - } - /** - * Creates a monaco instance with the given options. - * - * @param {Object} options Options used to initialize monaco. - * @param {Element} options.el The element which will be used to create the monacoEditor. + * Creates a Source Editor Instance with the given options. + * @param {Object} options Options used to initialize the instance. + * @param {Element} options.el The element to attach the instance for. * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language. * @param {string} options.blobContent The content to initialize the monacoEditor. + * @param {string} options.blobOriginalContent The original blob's content. Is used when creating a Diff Instance. * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath. + * @param {Boolean} options.isDiff Flag to enable creation of a Diff Instance? + * @param {...*} options.instanceOptions Configuration options used to instantiate an instance. + * @returns {EditorInstance} */ createInstance({ el = undefined, @@ -156,13 +119,18 @@ export default class SourceEditor { SourceEditor.prepareInstance(el); const createEditorFn = isDiff ? 'createDiffEditor' : 'create'; - const instance = SourceEditor.convertMonacoToELInstance( + const instance = new EditorInstance( monacoEditor[createEditorFn].call(this, el, { ...this.options, ...instanceOptions, }), + this.extensionsStore, ); + waitForCSSLoaded(() => { + instance.layout(); + }); + let model; if (instanceOptions.model !== null) { model = SourceEditor.createEditorModel({ @@ -176,8 +144,8 @@ export default class SourceEditor { } instance.onDidDispose(() => { - SourceEditor.instanceRemoveFromRegistry(this, instance); - SourceEditor.instanceDisposeModels(this, instance, model); + instanceRemoveFromRegistry(this, instance); + instanceDisposeModels(this, instance, model); }); this.instances.push(instance); @@ -185,6 +153,11 @@ export default class SourceEditor { return instance; } + /** + * Create a Diff Instance + * @param {Object} args Options to be passed further down to createInstance() with the same signature + * @returns {EditorInstance} + */ createDiffInstance(args) { return this.createInstance({ ...args, @@ -192,6 +165,10 @@ export default class SourceEditor { }); } + /** + * Dispose global editor + * Automatically disposes all the instances registered for this editor + */ dispose() { this.instances.forEach((instance) => instance.dispose()); } diff --git a/app/assets/javascripts/editor/source_editor_extension.js b/app/assets/javascripts/editor/source_editor_extension.js index f6bc62a1c09..6d47e1e2248 100644 --- a/app/assets/javascripts/editor/source_editor_extension.js +++ b/app/assets/javascripts/editor/source_editor_extension.js @@ -5,10 +5,10 @@ export default class EditorExtension { 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(); + this.extensionName = definition.extensionName || this.obj.extensionName; // both class- and fn-based extensions have a name } get api() { diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js index fcffdc587be..8372a59964b 100644 --- a/app/assets/javascripts/editor/source_editor_instance.js +++ b/app/assets/javascripts/editor/source_editor_instance.js @@ -13,7 +13,7 @@ * A Source Editor Extension * @typedef {Object} SourceEditorExtension * @property {Object} obj - * @property {string} name + * @property {string} extensionName * @property {Object} api */ @@ -43,12 +43,12 @@ const utils = { } }, - getStoredExtension: (extensionsStore, name) => { + getStoredExtension: (extensionsStore, extensionName) => { if (!extensionsStore) { logError(EDITOR_EXTENSION_STORE_IS_MISSING_ERROR); return undefined; } - return extensionsStore.get(name); + return extensionsStore.get(extensionName); }, }; @@ -73,30 +73,18 @@ export default class EditorInstance { if (methodExtension) { const extension = extensionsStore.get(methodExtension); - return (...args) => extension.api[prop].call(seInstance, receiver, ...args); + if (typeof extension.api[prop] === 'function') { + return extension.api[prop].bind(extension.obj, receiver); + } + + return extension.api[prop]; } return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver); }, - set(target, prop, value) { - Object.assign(seInstance, { - [prop]: value, - }); - return true; - }, }; const instProxy = new Proxy(rootInstance, getHandler); - /** - * Main entry point to apply an extension to the instance - * @param {SourceEditorExtensionDefinition} - */ - this.use = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.useExtension); - - /** - * Main entry point to un-use an extension and remove it from the instance - * @param {SourceEditorExtension} - */ - this.unuse = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.unuseExtension); + this.dispatchExtAction = EditorInstance.useUnuse.bind(instProxy, extensionsStore); return instProxy; } @@ -141,7 +129,7 @@ export default class EditorInstance { } // Existing Extension Path - const existingExt = utils.getStoredExtension(extensionsStore, definition.name); + const existingExt = utils.getStoredExtension(extensionsStore, definition.extensionName); if (existingExt) { if (isEqual(extension.setupOptions, existingExt.setupOptions)) { return existingExt; @@ -168,14 +156,14 @@ export default class EditorInstance { * @param {Map} extensionsStore - The global registry for the extension instances */ registerExtension(extension, extensionsStore) { - const { name } = extension; + const { extensionName } = extension; const hasExtensionRegistered = - extensionsStore.has(name) && - isEqual(extension.setupOptions, extensionsStore.get(name).setupOptions); + extensionsStore.has(extensionName) && + isEqual(extension.setupOptions, extensionsStore.get(extensionName).setupOptions); if (hasExtensionRegistered) { return; } - extensionsStore.set(name, extension); + extensionsStore.set(extensionName, extension); const { obj: extensionObj } = extension; if (extensionObj.onUse) { extensionObj.onUse(this); @@ -187,7 +175,7 @@ export default class EditorInstance { * @param {SourceEditorExtension} extension - Instance of Source Editor extension */ registerExtensionMethods(extension) { - const { api, name } = extension; + const { api, extensionName } = extension; if (!api) { return; @@ -197,7 +185,7 @@ export default class EditorInstance { if (this[prop]) { logError(sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop })); } else { - this.methods[prop] = name; + this.methods[prop] = extensionName; } }, this); } @@ -215,10 +203,10 @@ export default class EditorInstance { if (!extension) { throw new Error(EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR); } - const { name } = extension; - const existingExt = utils.getStoredExtension(extensionsStore, name); + const { extensionName } = extension; + const existingExt = utils.getStoredExtension(extensionsStore, extensionName); if (!existingExt) { - throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name })); + throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { extensionName })); } const { obj: extensionObj } = existingExt; if (extensionObj.onBeforeUnuse) { @@ -235,12 +223,12 @@ export default class EditorInstance { * @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use */ unregisterExtensionMethods(extension) { - const { api, name } = extension; + const { api, extensionName } = extension; if (!api) { return; } Object.keys(api).forEach((method) => { - utils.removeExtFromMethod(method, name, this.methods); + utils.removeExtFromMethod(method, extensionName, this.methods); }); } @@ -260,6 +248,24 @@ export default class EditorInstance { } /** + * Main entry point to apply an extension to the instance + * @param {SourceEditorExtensionDefinition[]|SourceEditorExtensionDefinition} extDefs - The extension(s) to use + * @returns {EditorExtension|*} + */ + use(extDefs) { + return this.dispatchExtAction(this.useExtension, extDefs); + } + + /** + * Main entry point to remove an extension to the instance + * @param {SourceEditorExtension[]|SourceEditorExtension} exts - + * @returns {*} + */ + unuse(exts) { + return this.dispatchExtAction(this.unuseExtension, exts); + } + + /** * Get the methods returned by extensions. * @returns {Array} */ |