2020-11-24 15:15:51 +05:30
|
|
|
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { uuids } from '~/diffs/utils/uuids';
|
2020-03-13 15:44:24 +05:30
|
|
|
import { defaultEditorOptions } from '~/ide/lib/editor_options';
|
2021-03-11 19:13:27 +05:30
|
|
|
import languages from '~/ide/lib/languages';
|
|
|
|
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
|
2020-05-24 23:13:21 +05:30
|
|
|
import { registerLanguages } from '~/ide/utils';
|
2020-10-24 23:57:45 +05:30
|
|
|
import { joinPaths } from '~/lib/utils/url_utility';
|
2021-03-11 19:13:27 +05:30
|
|
|
import {
|
|
|
|
EDITOR_LITE_INSTANCE_ERROR_NO_EL,
|
|
|
|
URI_PREFIX,
|
|
|
|
EDITOR_READY_EVENT,
|
|
|
|
EDITOR_TYPE_DIFF,
|
|
|
|
} from './constants';
|
2020-03-13 15:44:24 +05:30
|
|
|
import { clearDomElement } from './utils';
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
export default class EditorLite {
|
2020-03-13 15:44:24 +05:30
|
|
|
constructor(options = {}) {
|
2020-11-24 15:15:51 +05:30
|
|
|
this.instances = [];
|
2020-03-13 15:44:24 +05:30
|
|
|
this.options = {
|
2020-04-08 14:13:33 +05:30
|
|
|
extraEditorClassName: 'gl-editor-lite',
|
2020-03-13 15:44:24 +05:30
|
|
|
...defaultEditorOptions,
|
|
|
|
...options,
|
|
|
|
};
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
EditorLite.setupMonacoTheme();
|
2020-05-24 23:13:21 +05:30
|
|
|
|
|
|
|
registerLanguages(...languages);
|
2020-03-13 15:44:24 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
static setupMonacoTheme() {
|
2020-04-08 14:13:33 +05:30
|
|
|
const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
|
2021-03-08 18:12:59 +05:30
|
|
|
const theme = themes.find((t) => t.name === themeName);
|
2020-04-08 14:13:33 +05:30
|
|
|
if (theme) monacoEditor.defineTheme(themeName, theme.data);
|
|
|
|
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
|
2020-03-13 15:44:24 +05:30
|
|
|
}
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
static getModelLanguage(path) {
|
2020-11-24 15:15:51 +05:30
|
|
|
const ext = `.${path.split('.').pop()}`;
|
|
|
|
const language = monacoLanguages
|
|
|
|
.getLanguages()
|
2021-03-08 18:12:59 +05:30
|
|
|
.find((lang) => lang.extensions.indexOf(ext) !== -1);
|
2021-03-11 19:13:27 +05:30
|
|
|
return language ? language.id : 'plaintext';
|
2020-11-24 15:15:51 +05:30
|
|
|
}
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
static pushToImportsArray(arr, toImport) {
|
|
|
|
arr.push(import(toImport));
|
|
|
|
}
|
|
|
|
|
|
|
|
static loadExtensions(extensions) {
|
|
|
|
if (!extensions) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
const promises = [];
|
|
|
|
const extensionsArray = typeof extensions === 'string' ? extensions.split(',') : extensions;
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
extensionsArray.forEach((ext) => {
|
2021-01-03 14:25:43 +05:30
|
|
|
const prefix = ext.includes('/') ? '' : 'editor/';
|
|
|
|
const trimmedExt = ext.replace(/^\//, '').trim();
|
2021-02-22 17:27:13 +05:30
|
|
|
EditorLite.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
|
2021-01-03 14:25:43 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
return Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
static mixIntoInstance(source, inst) {
|
|
|
|
if (!inst) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const isClassInstance = source.constructor.prototype !== Object.prototype;
|
|
|
|
const sanitizedSource = isClassInstance ? source.constructor.prototype : source;
|
2021-03-08 18:12:59 +05:30
|
|
|
Object.getOwnPropertyNames(sanitizedSource).forEach((prop) => {
|
2021-02-22 17:27:13 +05:30
|
|
|
if (prop !== 'constructor') {
|
|
|
|
Object.assign(inst, { [prop]: source[prop] });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
static prepareInstance(el) {
|
2020-11-24 15:15:51 +05:30
|
|
|
if (!el) {
|
|
|
|
throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
|
2020-10-24 23:57:45 +05:30
|
|
|
}
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
clearDomElement(el);
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
monacoEditor.onDidCreateEditor(() => {
|
|
|
|
delete el.dataset.editorLoading;
|
|
|
|
});
|
2021-03-11 19:13:27 +05:30
|
|
|
}
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
static manageDefaultExtensions(instance, el, extensions) {
|
2021-02-22 17:27:13 +05:30
|
|
|
EditorLite.loadExtensions(extensions, instance)
|
2021-03-08 18:12:59 +05:30
|
|
|
.then((modules) => {
|
2021-01-03 14:25:43 +05:30
|
|
|
if (modules) {
|
2021-03-08 18:12:59 +05:30
|
|
|
modules.forEach((module) => {
|
2021-01-03 14:25:43 +05:30
|
|
|
instance.use(module.default);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(() => {
|
2021-03-11 19:13:27 +05:30
|
|
|
el.dispatchEvent(new Event(EDITOR_READY_EVENT));
|
2021-01-03 14:25:43 +05:30
|
|
|
})
|
2021-03-08 18:12:59 +05:30
|
|
|
.catch((e) => {
|
2021-01-03 14:25:43 +05:30
|
|
|
throw e;
|
|
|
|
});
|
2021-03-11 19:13:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
static createEditorModel({
|
|
|
|
blobPath,
|
|
|
|
blobContent,
|
|
|
|
blobOriginalContent,
|
|
|
|
blobGlobalId,
|
|
|
|
instance,
|
|
|
|
isDiff,
|
|
|
|
} = {}) {
|
|
|
|
if (!instance) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
|
|
|
|
const uri = Uri.file(uriFilePath);
|
|
|
|
const existingModel = monacoEditor.getModel(uri);
|
|
|
|
const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
|
|
|
|
if (!isDiff) {
|
|
|
|
instance.setModel(model);
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
const diffModel = {
|
|
|
|
original: monacoEditor.createModel(
|
|
|
|
blobOriginalContent,
|
|
|
|
EditorLite.getModelLanguage(model.uri.path),
|
|
|
|
),
|
|
|
|
modified: model,
|
|
|
|
};
|
|
|
|
instance.setModel(diffModel);
|
|
|
|
return diffModel;
|
|
|
|
}
|
|
|
|
|
|
|
|
static convertMonacoToELInstance = (inst) => {
|
|
|
|
const editorLiteInstanceAPI = {
|
|
|
|
updateModelLanguage: (path) => {
|
|
|
|
return EditorLite.instanceUpdateLanguage(inst, path);
|
|
|
|
},
|
|
|
|
use: (exts = []) => {
|
|
|
|
return EditorLite.instanceApplyExtension(inst, exts);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const handler = {
|
|
|
|
get(target, prop, receiver) {
|
|
|
|
if (Reflect.has(editorLiteInstanceAPI, prop)) {
|
|
|
|
return editorLiteInstanceAPI[prop];
|
|
|
|
}
|
|
|
|
return Reflect.get(target, prop, receiver);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return new Proxy(inst, handler);
|
|
|
|
};
|
|
|
|
|
|
|
|
static instanceUpdateLanguage(inst, path) {
|
|
|
|
const lang = EditorLite.getModelLanguage(path);
|
|
|
|
const model = inst.getModel();
|
|
|
|
return monacoEditor.setModelLanguage(model, lang);
|
|
|
|
}
|
|
|
|
|
|
|
|
static instanceApplyExtension(inst, exts = []) {
|
|
|
|
const extensions = [].concat(exts);
|
|
|
|
extensions.forEach((extension) => {
|
|
|
|
EditorLite.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.
|
|
|
|
* @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.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
|
|
|
|
*/
|
|
|
|
createInstance({
|
|
|
|
el = undefined,
|
|
|
|
blobPath = '',
|
|
|
|
blobContent = '',
|
|
|
|
blobOriginalContent = '',
|
|
|
|
blobGlobalId = uuids()[0],
|
|
|
|
extensions = [],
|
|
|
|
isDiff = false,
|
|
|
|
...instanceOptions
|
|
|
|
} = {}) {
|
|
|
|
EditorLite.prepareInstance(el);
|
|
|
|
|
|
|
|
const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
|
|
|
|
const instance = EditorLite.convertMonacoToELInstance(
|
|
|
|
monacoEditor[createEditorFn].call(this, el, {
|
|
|
|
...this.options,
|
|
|
|
...instanceOptions,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
let model;
|
|
|
|
if (instanceOptions.model !== null) {
|
|
|
|
model = EditorLite.createEditorModel({
|
|
|
|
blobGlobalId,
|
|
|
|
blobOriginalContent,
|
|
|
|
blobPath,
|
|
|
|
blobContent,
|
|
|
|
instance,
|
|
|
|
isDiff,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
instance.onDidDispose(() => {
|
|
|
|
EditorLite.instanceRemoveFromRegistry(this, instance);
|
|
|
|
EditorLite.instanceDisposeModels(this, instance, model);
|
|
|
|
});
|
|
|
|
|
|
|
|
EditorLite.manageDefaultExtensions(instance, el, extensions);
|
2020-07-28 23:09:34 +05:30
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
this.instances.push(instance);
|
|
|
|
return instance;
|
2020-07-28 23:09:34 +05:30
|
|
|
}
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
createDiffInstance(args) {
|
|
|
|
return this.createInstance({
|
|
|
|
...args,
|
|
|
|
isDiff: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
dispose() {
|
2021-03-08 18:12:59 +05:30
|
|
|
this.instances.forEach((instance) => instance.dispose());
|
2020-07-28 23:09:34 +05:30
|
|
|
}
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
use(exts) {
|
|
|
|
this.instances.forEach((inst) => {
|
|
|
|
inst.use(exts);
|
|
|
|
});
|
|
|
|
return this;
|
2020-03-13 15:44:24 +05:30
|
|
|
}
|
|
|
|
}
|