node-red/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js

1457 lines
74 KiB
JavaScript

/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* @namespace RED.editor.codeEditor.monaco
*/
/*
The code editor currenlty supports 2 functions init and create.
* Init() - setup the editor / must return true
* Create() - create an editor instance / returns an editor as generated by the editor lib
* To be compatable with the original ace lib (for contrib nodes using it), the object returned by create() must (at minimum) support the following...
property .selection = {};
function .selection.getRange();
property .session //the editor object
function .session.insert = function(position, text)
function .setReadOnly(readOnly)
property .renderer = {};
function .renderer.updateFull()
function setMode(mode, cb)
function getRange();
function replace(range, text)
function selectAll
function clearSelection
function getSelectedText()
function destroy()
function resize()
function getSession()
function getLength()
function scrollToLine(lineNumber, scrollType)
function moveCursorTo(lineNumber, colNumber)
function getAnnotations()
function gotoLine(row, col)
function getCursorPosition()
function setTheme(name)
function setFontSize(size:Number) //Set a new font size (in pixels) for the editor text.
function on(name, cb)
function getUndoManager()
*/
RED.editor.codeEditor.monaco = (function() {
var initialised = false;
const type = "monaco";
const monacoThemes = ["vs","vs-dark","hc-black"]; //TODO: consider setting hc-black autmatically based on acessability?
let userSelectedTheme;
//TODO: get from externalModules.js For now this is enough for feature parity with ACE (and then some).
const knownModules = {
"assert": {package: "node", module: "assert", path: "node/assert.d.ts" },
"assert/strict": {package: "node", module: "assert/strict", path: "node/assert/strict.d.ts" },
"async_hooks": {package: "node", module: "async_hooks", path: "node/async_hooks.d.ts" },
"buffer": {package: "node", module: "buffer", path: "node/buffer.d.ts" },
"child_process": {package: "node", module: "child_process", path: "node/child_process.d.ts" },
"cluster": {package: "node", module: "cluster", path: "node/cluster.d.ts" },
"console": {package: "node", module: "console", path: "node/console.d.ts" },
"crypto": {package: "node", module: "crypto", path: "node/crypto.d.ts" },
"dgram": {package: "node", module: "dgram", path: "node/dgram.d.ts" },
"diagnostics_channel.d": {package: "node", module: "diagnostics_channel", path: "node/diagnostics_channel.d.ts" },
"dns": {package: "node", module: "dns", path: "node/dns.d.ts" },
"dns/promises": {package: "node", module: "dns/promises", path: "node/dns/promises.d.ts" },
"domain": {package: "node", module: "domain", path: "node/domain.d.ts" },
"events": {package: "node", module: "events", path: "node/events.d.ts" },
"fs": {package: "node", module: "fs", path: "node/fs.d.ts" },
"fs/promises": {package: "node", module: "fs/promises", path: "node/fs/promises.d.ts" },
"globals": {package: "node", module: "globals", path: "node/globals.d.ts" },
"http": {package: "node", module: "http", path: "node/http.d.ts" },
"http2": {package: "node", module: "http2", path: "node/http2.d.ts" },
"https": {package: "node", module: "https", path: "node/https.d.ts" },
"module": {package: "node", module: "module", path: "node/module.d.ts" },
"net": {package: "node", module: "net", path: "node/net.d.ts" },
"os": {package: "node", module: "os", path: "node/os.d.ts" },
"path": {package: "node", module: "path", path: "node/path.d.ts" },
"perf_hooks": {package: "node", module: "perf_hooks", path: "node/perf_hooks.d.ts" },
"process": {package: "node", module: "process", path: "node/process.d.ts" },
"querystring": {package: "node", module: "querystring", path: "node/querystring.d.ts" },
"readline": {package: "node", module: "readline", path: "node/readline.d.ts" },
"stream": {package: "node", module: "stream", path: "node/stream.d.ts" },
"stream/consumers": {package: "node", module: "stream/consumers", path: "node/stream/consumers.d.ts" },
"stream/promises": {package: "node", module: "stream/promises", path: "node/stream/promises.d.ts" },
"stream/web": {package: "node", module: "stream/web", path: "node/stream/web.d.ts" },
"string_decoder": {package: "node", module: "string_decoder", path: "node/string_decoder.d.ts" },
"test": {package: "node", module: "test", path: "node/test.d.ts" },
"timers": {package: "node", module: "timers", path: "node/timers.d.ts" },
"timers/promises": {package: "node", module: "timers/promises", path: "node/timers/promises.d.ts" },
"tls": {package: "node", module: "tls", path: "node/tls.d.ts" },
"trace_events": {package: "node", module: "trace_events", path: "node/trace_events.d.ts" },
"tty": {package: "node", module: "tty", path: "node/tty.d.ts" },
"url": {package: "node", module: "url", path: "node/url.d.ts" },
"util": {package: "node", module: "util", path: "node/util.d.ts" },
"v8": {package: "node", module: "v8", path: "node/v8.d.ts" },
"vm": {package: "node", module: "vm", path: "node/vm.d.ts" },
"wasi": {package: "node", module: "wasi", path: "node/wasi.d.ts" },
"worker_threads": {package: "node", module: "worker_threads", path: "node/worker_threads.d.ts" },
"zlib": {package: "node", module: "zlib", path: "node/zlib.d.ts" },
"node-red": {package: "node-red", module: "node-red", path: "node-red/index.d.ts" }, //only used if node-red types are concated by grunt.
"node-red-util": {package: "node-red", module: "util", path: "node-red/util.d.ts" },
"node-red-func": {package: "node-red", module: "func", path: "node-red/func.d.ts" },
}
const defaultServerSideTypes = [ knownModules["node-red-util"], knownModules["node-red-func"], knownModules["globals"], knownModules["console"], knownModules["buffer"], knownModules["timers"] , knownModules["util"] ];
const modulesCache = {};
/**
* Helper function to load/reload types.
* @param {string} mod - type lib to load. Only known libs are currently supported.
* @param {boolean} preloadOnly - only cache the lib
* @param {object} loadedLibs - an object used to track loaded libs (needed to destroy them upon close)
*/
function _loadModuleDTS(mod, preloadOnly, loadedLibs, cb) {
var _module;
if(typeof mod == "object") {
_module = mod;
} else {
_module = knownModules[mod];
}
if(_module) {
const libPackage = _module.package;
const libModule = _module.module;
const libPath = _module.path;
const def = modulesCache[libPath];
if( def ) {
if(!preloadOnly) {
loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(def, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
}
if(cb) {
setTimeout(function() {
cb(null, _module);
}, 5);
}
} else {
var typePath = "types/" + libPath;
$.get(typePath)
.done(function(data) {
modulesCache[libPath] = data;
if(!preloadOnly) {
loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
}
if(cb) { cb(null, _module) }
})
.fail(function(err) {
var warning = "Failed to load '" + typePath + "'";
modulesCache[libPath] = "/* " + warning + " */\n"; //populate the extraLibs cache to revent retries
if(cb) { cb(err, _module) }
console.warn(warning);
});
}
}
}
function init(options) {
//Handles orphaned models
//ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed
RED.events.on("editor:close",function() {
let models = window.monaco ? monaco.editor.getModels() : null;
if(models && models.length) {
console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().")
for (let index = 0; index < models.length; index++) {
const model = models[index];
if(!model.isDisposed()) {
model.dispose();
}
}
}
});
options = options || {};
window.MonacoEnvironment = window.MonacoEnvironment || {};
window.MonacoEnvironment.getWorkerUrl = window.MonacoEnvironment.getWorkerUrl || function (moduleId, label) {
if (label === 'json') { return './vendor/monaco/dist/json.worker.js'; }
if (label === 'css' || label === 'scss') { return './vendor/monaco/dist/css.worker.js'; }
if (label === 'html' || label === 'handlebars') { return './vendor/monaco/dist/html.worker.js'; }
if (label === 'typescript' || label === 'javascript') { return './vendor/monaco/dist/ts.worker.js'; }
return './vendor/monaco/dist/editor.worker.js';
};
var editorSettings = RED.editor.codeEditor.settings || {};
var editorOptions = editorSettings.options || {};
//if editorOptions.theme is an object (set in theme.js context()), use the plugin theme name as the monaco theme name
//if editorOptions.theme is a string, it should be the name of a pre-set theme, load that
try {
const addTheme = function (themeThemeName, theme) {
if ((theme.rules && Array.isArray(theme.rules)) || theme.colors) {
monacoThemes.push(themeThemeName); //add to list of loaded themes
monaco.editor.defineTheme(themeThemeName, theme);
monaco.editor.setTheme(themeThemeName);
userSelectedTheme = themeThemeName;
}
};
if (editorOptions.theme) {
if (typeof editorOptions.theme == "object" && RED.settings.editorTheme.theme) {
let themeThemeName = editorOptions.theme.name || RED.settings.editorTheme.theme;
addTheme(themeThemeName, editorOptions.theme);
} else if (typeof editorOptions.theme == "string") {
let themeThemeName = editorOptions.theme;
if (!monacoThemes.includes(themeThemeName)) {
$.get('vendor/monaco/dist/theme/' + themeThemeName + '.json', function (theme) {
addTheme(themeThemeName, theme);
});
}
}
}
} catch (error) {
console.warn(error);
}
//Helper function to simplify snippet setup
function createMonacoCompletionItem(label, insertText, documentation, range, kind) {
if (Array.isArray(documentation)) { documentation = documentation.join("\n"); }
return {
label: label,
kind: kind == null ? monaco.languages.CompletionItemKind.Snippet : kind,
documentation: { value: documentation },
insertText: insertText,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
}
}
function setupJSONata(_monaco) {
// Register the language 'jsonata'
_monaco.languages.register({ id: 'jsonata' });
// Setup tokens for JSONata
_monaco.languages.setMonarchTokensProvider('jsonata',
{
// Set defaultToken to invalid to see what you do not tokenize yet
defaultToken: 'invalid',
tokenPostfix: '.js',
keywords: ["function", "true", "true", "null", "Infinity", "NaN", "undefined"].concat(Object.keys(jsonata.functions)),
// keywords: [
// "function", "$abs", "$append", "$assert", "$average",
// "$base64decode", "$base64encode", "$boolean", "$ceil", "$contains",
// "$count", "$decodeUrl", "$decodeUrlComponent", "$distinct", "$each", "$encodeUrl",
// "$encodeUrlComponent", "$env", "$error", "$eval", "$exists", "$filter", "$floor",
// "$flowContext", "$formatBase", "$formatInteger", "$formatNumber", "$fromMillis",
// "$globalContext", "$join", "$keys", "$length", "$lookup", "$lowercase", "$map",
// "$match", "$max", "$merge", "$millis", "$min", "$moment", "$not", "$now",
// "$number", "$pad", "$parseInteger", "$power", "$random", "$reduce", "$replace",
// "$reverse", "$round", "$shuffle", "$sift", "$single", "$sort", "$split",
// "$spread", "$sqrt", "$string", "$substring", "$substringAfter", "$substringBefore",
// "$sum", "$toMillis", "$trim", "$type", "$uppercase", "$zip"
// ],
operatorsKeywords: [ 'and', 'or', 'in' ],
operators: [
'<=', '>=', '!=', '==', '!=', '=>', '+', '-', '*', '/', '%',
':=', '~>', '?', ':', '..', '@', '#', '|', '^', '*', '**',
],
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%@#]+/,
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
digits: /\d+(_+\d+)*/,
octaldigits: /[0-7]+(_+[0-7]+)*/,
binarydigits: /[0-1]+(_+[0-1]+)*/,
hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
regexpesc: /\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,
// The main tokenizer
tokenizer: {
root: [
[/[{}]/, 'delimiter.bracket'],
{ include: 'common' }
],
common: [
// identifiers and keywords
[/([a-zA-Z][\w$]*)|([$][\w$]*)/, {
cases: {
'@keywords': 'keyword',
'@operatorsKeywords': 'keyword',
'$2': 'variable', //when its not a key word but starts with $, use "tag" for colourisation
//'$2': 'tag', //when its not a key word but starts with $, use "tag" for colourisation
//'$2': 'constant', //alt colourisation
//'$2': 'attribute', //alt colourisation
//'$2': 'identifier.variable', //alt custom colourisation
'@default': 'identifier'
}
}],
[/[$][\w\$]*/, 'variable'],
// whitespace
{ include: '@whitespace' },
// regular expression: ensure it is terminated before beginning (otherwise it is an opeator)
[/\/(?=([^\\\/]|\\.)+\/([gimsuy]*)(\s*)(\.|;|\/|,|\)|\]|\}|$))/, { token: 'regexp', bracket: '@open', next: '@regexp' }],
// delimiters and operators
[/[()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/(@symbols)|(\.\.)/, {
cases: {
'@operators': 'operator',
'@default': ''
}
}],
// numbers
[/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
[/0[xX](@hexdigits)/, 'number.hex'],
[/0[oO]?(@octaldigits)/, 'number.octal'],
[/0[bB](@binarydigits)/, 'number.binary'],
[/(@digits)/, 'number'],
// delimiter: after number because of .\d floats
[/[?:;,.]/, 'delimiter'],
// strings
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/"/, 'string', '@string_double'],
[/'/, 'string', '@string_single'],
[/`/, 'string', '@string_backtick'],
],
whitespace: [
[/[ \t\r\n]+/, ''],
[/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment'],
],
comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment']
],
jsdoc: [
[/[^\/*]+/, 'comment.doc'],
[/\*\//, 'comment.doc', '@pop'],
[/[\/*]/, 'comment.doc']
],
// We match regular expression quite precisely
regexp: [
[/(\{)(\d+(?:,\d*)?)(\})/, ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']],
[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, ['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]],
[/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
[/[()]/, 'regexp.escape.control'],
[/@regexpctl/, 'regexp.escape.control'],
[/[^\\\/]/, 'regexp'],
[/@regexpesc/, 'regexp.escape'],
[/\\\./, 'regexp.invalid'],
[/(\/)([gimsuy]*)/, [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']],
],
regexrange: [
[/-/, 'regexp.escape.control'],
[/\^/, 'regexp.invalid'],
[/@regexpesc/, 'regexp.escape'],
[/[^\]]/, 'regexp'],
[/\]/, { token: 'regexp.escape.control', next: '@pop', bracket: '@close' }],
],
string_double: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop']
],
string_single: [
[/[^\\']+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop']
],
string_backtick: [
[/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
[/[^\\`$]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/`/, 'string', '@pop']
],
bracketCounting: [
[/\{/, 'delimiter.bracket', '@bracketCounting'],
[/\}/, 'delimiter.bracket', '@pop'],
{ include: 'common' }
],
},
}
);
// Setup JSONata language config
_monaco.languages.setLanguageConfiguration('jsonata', {
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string'] }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '<', close: '>' }
],
folding: {
markers: {
start: new RegExp('^\\s*//\\s*(?:(?:#?region\\b)|(?:<editor-fold\\b))'),
end: new RegExp('^\\s*//\\s*(?:(?:#?endregion\\b)|(?:</editor-fold>))')
}
}
});
// Register a completion item provider for JSONata snippets
_monaco.languages.registerCompletionItemProvider('jsonata', {
provideCompletionItems: function (model, position) {
var _word = model.getWordUntilPosition(position);
if (!_word) { return; }
var startColumn = _word.startColumn;
var word = _word.word;
if (word[0] !== "$" && position.column > 1) { startColumn--; }
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: startColumn,
endColumn: _word.endColumn
};
var jsonataFunctions = Object.keys(jsonata.functions);
var jsonataSuggestions = jsonataFunctions.map(function (f) {
var args = RED._('jsonata:' + f + '.args', { defaultValue: '' });
var title = f + '(' + args + ')';
var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });
var insertText = (jsonata.getFunctionSnippet(f) + '').trim();
var documentation = { value: '`' + title + '`\n\n' + body };
return createMonacoCompletionItem(f, insertText, documentation, range, monaco.languages.CompletionItemKind.Function);
});
// sort in length order (long->short) otherwise substringAfter gets matched as substring etc.
jsonataFunctions.sort(function (A, B) {
return B.length - A.length;
});
// add snippets to suggestions
jsonataSuggestions.unshift(
createMonacoCompletionItem("randominteger", '(\n\t\\$minimum := ${1:1};\n\t\\$maximum := ${2:10};\n\t\\$round((\\$random() * (\\$maximum-\\$minimum)) + \\$minimum, 0)\n)', 'Random integer between 2 numbers', range)
);//TODO: add more JSONata snippets
return { suggestions: jsonataSuggestions };
}
});
// Register a hover provider for JSONata functions
_monaco.languages.registerHoverProvider('jsonata', {
provideHover: function (model, position) {
var w = model.getWordAtPosition(position);
var f = w && w.word;
if (!f) {return;}
if (f[0] !== "$" && position.column > 1) {
f = "$" + f;
} else {
return;
}
var args = RED._('jsonata:' + f + ".args", { defaultValue: '' });
if (!args) {return;}
var title = f + "(" + args + ")";
var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });
/** @type {monaco.Range} */
var r = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column + w.word.length);
return {
range: r,
contents: [
{ value: '**`' + title + '`**' },
// { value: '```html\n' + body + '\n```' },
{ value: body },
]
}
}
});
}
function setupJSON(_monaco) {
//Setup JSON options
try {
var diagnosticOptionsDefault = {validate: true};
var diagnosticOptions = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.diagnosticOptions');
var modeConfiguration = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.modeConfiguration');
diagnosticOptions = Object.assign({}, diagnosticOptionsDefault, (diagnosticOptions || {}));
_monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up json options", err)
}
}
function setupHTML(_monaco) {
//Setup HTML / Handlebars options
try {
var htmlDefaults = RED.settings.get('codeEditor.monaco.languages.html.htmlDefaults.options');
var handlebarDefaults = RED.settings.get('codeEditor.monaco.languages.html.handlebarDefaults.options');
if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
} catch (error) {
console.warn("monaco - Error setting up html options", err)
}
}
function setupCSS(_monaco) {
//Setup CSS/SCSS/LESS options
try {
var cssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.diagnosticsOptions');
var lessDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.diagnosticsOption');
var scssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.diagnosticsOption');
var cssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.modeConfiguration');
var lessDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.modeConfiguration');
var scssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.modeConfiguration');
if(cssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_diagnosticsOption); }
if(lessDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_diagnosticsOption); }
if(scssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_diagnosticsOption); }
if(cssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_modeConfiguration); }
if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up CSS/SCSS/LESS options", err)
}
}
function setupJS(_monaco) {
var createJSSnippets = function(range) {
return [
//TODO: i18n for snippet descriptions?
createMonacoCompletionItem("dowhile", 'do {\n\t${2}\n} while (${1:condition});','Do-While Statement (JavaScript Language Basics)',range),
createMonacoCompletionItem("while", 'while (${1:condition}) {\n\t${2}\n}','While Statement (JavaScript Language Basics)',range),
createMonacoCompletionItem("switch", 'switch (${1:msg.topic}) {\n\tcase ${2:"value"}:\n\t\t${3}\n\t\tbreak;\n\tdefault:\n\t\t\n}','Switch Statement (JavaScript Language Basics)',range),
createMonacoCompletionItem("trycatch", 'try {\n\t${2}\n} catch (${1:error}) {\n\t\n};','Try-Catch Statement (JavaScript Language Basics)',range),
createMonacoCompletionItem("for (for loop)", 'for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {\n\tconst element = ${2:array}[${1:index}];\n\t${3}\n}','for loop',range),
createMonacoCompletionItem("foreach", '${1:array}.forEach(function(${2:element}) {\n\t${3}\n});','forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void\n\nA function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.',range),
createMonacoCompletionItem("forin", 'for (${1:prop} in ${2:obj}) {\n\tif (${2:obj}.hasOwnProperty(${1:prop})) {\n\t\t${3}\n\t}\n}','for in',range),
createMonacoCompletionItem("forof", 'for (const ${1:iterator} of ${2:object}) {\n\t${3}\n}','for of',range),
createMonacoCompletionItem("function", 'function ${1:methodName}(${2:arguments}) {\n\t${3}\n}','Function Declaration',range),
createMonacoCompletionItem("func (anonymous function)", 'var ${1:fn} = function(${2:arguments}) {\n\t${3}\n}','Function Expression',range),
createMonacoCompletionItem("pt (prototype)", '${1:ClassName}.prototype.${2:methodName} = function(${3:arguments}) {\n\t${4}\n}','prototype',range),
createMonacoCompletionItem("iife", '(function(${1:arg}) {\n\t${1}\n})(${1:arg});','immediately-invoked function expression',range),
createMonacoCompletionItem("call (function call)", '${1:methodName}.call(${2:context}, ${3:arguments})','function call',range),
createMonacoCompletionItem("apply (function apply)", '${1:methodName}.apply(${2:context}, [${3:arguments}])','function apply',range),
createMonacoCompletionItem("jsonparse", 'JSON.parse(${1:json});','JSON.parse',range),
createMonacoCompletionItem("jsonstringify", 'JSON.stringify(${1:obj});','JSON.stringify',range),
createMonacoCompletionItem("setinterval", 'setInterval(function() {\n\t${2}\n}, ${1:delay});','setInterval',range),
createMonacoCompletionItem("settimeout", 'setTimeout(function() {\n\t${2}\n}, ${1:delay});','setTimeout',range),
createMonacoCompletionItem("node.log", 'node.log(${1:"info"});','Write an info message to the console (not sent to sidebar)',range),
createMonacoCompletionItem("node.warn", 'node.warn(${1:"my warning"});','Write a warning to the console and debug sidebar',range),
createMonacoCompletionItem("node.error", 'node.error(${1:"my error message"}, ${2:msg});','Send an error to the console and debug sidebar. To trigger a Catch node on the same tab, the function should call `node.error` with the original message as a second argument',range),
createMonacoCompletionItem("node.send", 'node.send(${1:msg});','async send a msg to the next node',range),
createMonacoCompletionItem("node.send (multiple)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([[${1:msg1}, ${3:msg2}]]);','send 1 or more messages out of 1 output',range),
createMonacoCompletionItem("node.send (multiple outputs)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([${1:msg1}, ${3:msg2}]);','send more than 1 message out of multiple outputs',range),
createMonacoCompletionItem("node.status", 'node.status({fill:"${1|red,green,yellow,blue,grey|}",shape:"${2|ring,dot|}",text:"${3:message}"});','Set the status icon and text underneath the function node',range),
createMonacoCompletionItem("get (node context)", 'context.get("${1:name}");','Get a value from node context',range),
createMonacoCompletionItem("set (node context)", 'context.set("${1:name}", ${1:value});','Set a value in node context',range),
createMonacoCompletionItem("get (flow context)", 'flow.get("${1:name}");','Get a value from flow context',range),
createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range),
createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range),
createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range),
createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range),
createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});',
["```typescript",
"RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T",
"```",
"Safely clones a message object. This handles msg.req/msg.res objects that must not be cloned\n",
"*@param* `msg` — the msg object\n"],
range),
createMonacoCompletionItem("getObjectProperty (RED.util)", 'RED.util.getObjectProperty(${1:msg},${2:prop});',
["```typescript",
"RED.util.getObjectProperty(msg: object, expr: string): any;",
"```",
"Gets a property of an object\n",
"*@param* `msg` — the msg object\n",
"*@param* `prop` — the msg object"],
range),
createMonacoCompletionItem("setObjectProperty (RED.util)", 'RED.util.setObjectProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
["```typescript",
"RED.util.setObjectProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
"```",
"Sets a property of an object\n",
"`msg` — the object\n",
"`prop` — the property expression\n",
"`value` — the value to set\n",
"`createMissing` — whether to create missing parent properties"],
range),
createMonacoCompletionItem("getMessageProperty (RED.util)", 'RED.util.getMessageProperty(${1:msg},${2:prop});',
["```typescript",
"RED.util.getMessageProperty(msg: object, expr: string): any;",
"```",
"Gets a property of an object\n",
"*@param* `msg` — the msg object\n",
"*@param* `prop` — the msg object"],
range),
createMonacoCompletionItem("setMessageProperty (RED.util)", 'RED.util.setMessageProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
["```typescript",
"RED.util.setMessageProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
"```",
"Sets a property of an object\n",
"`msg` — the object\n",
"`prop` — the property expression\n",
"`value` — the value to set\n",
"`createMissing` — whether to create missing parent properties"],
range),
];
}
//register snippets
_monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: function(model, position) {
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
return {
suggestions: createJSSnippets(range)
};
}
});
//setup JS/TS compiler & diagnostic options (defaults)
try {
//prepare compiler options
var compilerOptions = {
allowJs: true,
checkJs: true,
allowNonTsExtensions: true,
target: monaco.languages.typescript.ScriptTarget.ESNext,
strictNullChecks: false,
strictPropertyInitialization: true,
strictFunctionTypes: true,
strictBindCallApply: true,
useDefineForClassFields: true,//permit class static fields with private name to have initializer
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
typeRoots: ["types"],
lib: ["esnext"] //dont load DOM by default,
}
//apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions in settings.js
var settingsComilerOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions') || {};
compilerOptions = Object.assign({}, compilerOptions, settingsComilerOptions);
/** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setcompileroptions */
_monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);
//prepare diagnostic options (defaults)
var diagnosticOptions = {
noSemanticValidation: false,
noSyntaxValidation: false,
diagnosticCodesToIgnore: [
1108, //return not inside function
1375, //'await' expressions are only allowed at the top level of a file when that file is a module
1378, //Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher
//2304, //Cannot find name - this one is heavy handed and prevents user seeing stupid errors. Would provide better ACE feature parity (i.e. no need for declaration of vars) but misses lots of errors. Lets be bold & leave it out!
2307, //Cannot find module 'xxx' or its corresponding type declarations
2322, //Type 'unknown' is not assignable to type 'string'
2339, //property does not exist on
2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
7043, //i forget what this one is,
80001, //Convert to ES6 module
80004, //JSDoc types may be moved to TypeScript types.
]
};
//apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions settings.js
var settingsDiagnosticsOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions') || {};
diagnosticOptions = Object.assign({}, diagnosticOptions, settingsDiagnosticsOptions);
/** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setdiagnosticsoptions */
_monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticOptions);
} catch (error) {
console.warn("monaco - Error setting javascriptDefaults", error)
}
}
setupJS(monaco);
setupJSONata(monaco);
setupJSON(monaco);
setupCSS(monaco);
setupHTML(monaco);
defaultServerSideTypes.forEach(function(m) {
_loadModuleDTS(m, true)//pre-load common libs
})
initialised = true;
return initialised;
}
function create(options) {
var editorSettings = RED.editor.codeEditor.settings || {};
var loadedLibs = {JS:{}, TS:{}};//for tracking and later disposing of loaded type libs
var watchTimer;
var createThemeMenuOption = function (theme, keybinding) {
return {
// An unique identifier of the contributed action.
id: 'set-theme-' + theme,
// A label of the action that will be presented to the user.
label: RED._('monaco.setTheme') + ' ' + theme,
precondition: null,// A precondition for this action.
keybindingContext: keybinding || null,// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convinience
run: function (ed) {
//monaco.editor.setTheme(theme)
ed.setTheme(theme)
return null;
}
}
}
var convertAceModeToMonacoLang = function (mode) {
if (typeof mode == "object" && mode.path) {
mode = mode.path;
}
if (mode) {
mode = mode.replace("ace/mode/", "");
} else {
mode = "text";
}
switch (mode) {
case "nrjavascript":
case "mjs":
mode = "javascript";
break;
case "vue":
mode = "html";
break;
case "appcache":
case "sh":
case "bash":
mode = "shell";
break;
case "batchfile":
mode = "bat";
break;
case "protobuf":
mode = "proto";
break;
//TODO: add other compatability types.
}
return mode;
}
if(!options.stateId && options.stateId !== false) {
options.stateId = RED.editor.generateViewStateId("monaco", options, (options.mode || options.title || "").split("/").pop());
}
var el = options.element || $("#"+options.id)[0];
var toolbarRow = $("<div>").appendTo(el);
el = $("<div>").appendTo(el).addClass("red-ui-editor-text-container")[0];
var editorOptions = $.extend({}, editorSettings.options, options);
editorOptions.language = convertAceModeToMonacoLang(options.mode);
if(userSelectedTheme) {
editorOptions.theme = userSelectedTheme;//use user selected theme for this session
}
//by default, set javascript editors to text mode.
//when element becomes visible, it will be (re) set to javascript mode
//this is to ensure multiple editors sharing the model dont present its
//consts & lets to each other
if(editorOptions.language == "javascript") {
editorOptions._language = editorOptions.language;
editorOptions.language = "text"
}
//apply defaults
if (!editorOptions.minimap) {
editorOptions.minimap = {
enabled: true,
maxColumn: 50,
scale: 1,
showSlider: "mouseover",
renderCharacters: true
}
}
//common ACE flags - set equivelants in monaco
if(options.enableBasicAutocompletion === false) {
editorOptions.showSnippets = false;
editorOptions.quickSuggestions = false;
editorOptions.parameterHints = { enabled: false };
editorOptions.suggestOnTriggerCharacters = false;
editorOptions.acceptSuggestionOnEnter = "off";
editorOptions.tabCompletion = "off";
editorOptions.wordBasedSuggestions = false;
}
if (options.enableSnippets === false) { editorOptions.showSnippets = false; }
if (editorOptions.mouseWheelZoom == null) { editorOptions.mouseWheelZoom = true; }
if (editorOptions.suggestFontSize == null) { editorOptions.suggestFontSize = 12; }
if (editorOptions.formatOnPaste == null) { editorOptions.formatOnPaste = true; }
if (editorOptions.foldingHighlight == null) { editorOptions.foldingHighlight = true; }
if (editorOptions.foldStyle == null) { editorOptions.foldStyle = true; } //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#folding
if (editorOptions.readOnly != null) { editorOptions.readOnly = editorOptions.readOnly; }
if (editorOptions.lineNumbers === false) { editorOptions.lineNumbers = false; }
if (editorOptions.theme == null) { editorOptions.theme = monacoThemes[0]; }
if (editorOptions.mode == null) { editorOptions.mode = convertAceModeToMonacoLang(options.mode); }
if (editorOptions.automaticLayout == null) { editorOptions.automaticLayout = true; }
if (options.foldStyle) {
switch (options.foldStyle) {
case "none":
editorOptions.foldStyle = false;
editorOptions.foldingHighlight = false
break;
default:
editorOptions.foldStyle = true;
editorOptions.foldingHighlight = true;
break;
}
} else {
editorOptions.foldStyle = true;
editorOptions.foldingHighlight = true;
}
//others
editorOptions.roundedSelection = editorOptions.roundedSelection === false ? false : true; //default to true
editorOptions.contextmenu = editorOptions.contextmenu === false ? false : true; //(context menu enable) default to true
editorOptions.snippetSuggestions = editorOptions.enableSnippets === false ? false : true; //default to true //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#snippetsuggestions
editorOptions.value = options.value || "";
//if wordSeparators are not supplied, override default ones for the "j" langs
if(!editorOptions.wordSeparators) {
if (editorOptions.language == "jsonata" || editorOptions.language == "json" || editorOptions.language == "javascript") {
editorOptions.wordSeparators = "`~!@#%^&*()-=+[{]}\|;:'\",.<>/?"; //dont use $ as separator
}
}
//fixedOverflowWidgets should probably never be set to false
//fixedOverflowWidgets allows hover tips to be above other parts of UI
editorOptions.fixedOverflowWidgets = editorOptions.fixedOverflowWidgets === false ? false : true;
//#region Detect mobile / tablet and reduce functionality for small screen and lower power
var browser = RED.utils.getBrowserInfo();
if (browser.mobile || browser.tablet) {
editorOptions.minimap = { enabled: false };
editorOptions.formatOnType = false; //try to prevent cursor issues
editorOptions.formatOnPaste = false; //try to prevent cursor issues
editorOptions.disableMonospaceOptimizations = true; //speed up
editorOptions.columnSelection = false; //try to prevent cursor issues
editorOptions.matchBrackets = "never"; //speed up
editorOptions.maxTokenizationLineLength = 10000; //speed up //internal default is 20000
editorOptions.stopRenderingLineAfter = 2000; //speed up //internal default is 10000
editorOptions.roundedSelection = false; //speed up rendering
editorOptions.trimAutoWhitespace = false; //try to prevent cursor issues
editorOptions.parameterHints = { enabled: false };
editorOptions.suggestOnTriggerCharacters = false;//limit suggestions, user can still use ctrl+space
editorOptions.wordBasedSuggestions = false;
editorOptions.suggest = { maxVisibleSuggestions: 6 };
// editorOptions.codeLens = false;//If Necessary, disable this useful feature?
// editorOptions.quickSuggestions = false;//If Necessary, disable this useful feature?
// editorOptions.showSnippets = false; //If Necessary, disable this useful feature?
// editorOptions.acceptSuggestionOnEnter = "off"; //If Necessary, disable this useful feature?
// editorOptions.tabCompletion = "off"; //If Necessary, disable this useful feature?
if (!editorOptions.accessibilitySupport && browser.android) {
editorOptions.accessibilitySupport = "off"; //ref https://github.com/microsoft/pxt/pull/7099/commits/35fd3e969b0d5b68ca1e35809f96cea81ef243bc
}
}
//#endregion
//#region Load types for intellisense
//Determine if this instance is for client side or server side...
//if clientSideSuggestions option is not specified, check see if
//requested mode is "nrjavascript" or if options.globals are specifying RED or Buffer
var serverSideSuggestions = false;
if (options.clientSideSuggestions == null) {
if ( ((options.mode + "").indexOf("nrjavascript") >= 0) || (options.globals && (options.globals.RED || options.globals.Buffer )) ) {
serverSideSuggestions = true;
}
}
// compiler options - enable / disable server-side/client-side suggestions
var compilerOptions = monaco.languages.typescript.javascriptDefaults.getCompilerOptions();
if (serverSideSuggestions) {
compilerOptions.lib = ["esnext"]; //dont include DOM
defaultServerSideTypes.forEach(function(m) {
_loadModuleDTS(m, false, loadedLibs); //load common node+node-red types
})
} else {
compilerOptions.lib = ["esnext", "dom"];
}
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);
//check if extraLibs are to be loaded (e.g. fs or os)
refreshModuleLibs(editorOptions.extraLibs)
function refreshModuleLibs(extraModuleLibs) {
var defs = [];
var imports = [];
const id = "extraModuleLibs/index.d.ts";
const file = 'file://types/extraModuleLibs/index.d.ts';
if(!extraModuleLibs || extraModuleLibs.length == 0) {
loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(" ", file);
} else {
var loadList = [];
Array.prototype.push.apply(loadList, extraModuleLibs);//Use this instead of spread operator to prevent IE syntax error
var loadExtraModules = {};
for (let index = 0; index < extraModuleLibs.length; index++) {
const lib = extraModuleLibs[index];
const varName = lib.var;
const moduleName = lib.module;
if(varName && moduleName) {
imports.push("import " + varName + "_import = require('" + moduleName + "');\n");
defs.push("var " + varName + ": typeof " + varName + "_import;\n");
}
var km = knownModules[moduleName];
if(km) {
loadExtraModules[moduleName] = km;
} else {
loadExtraModules[moduleName] = {package: "other", module: moduleName, path: "other/" + moduleName + ".d.ts" };
}
}
Object.values(loadExtraModules).forEach(function(m) {
_loadModuleDTS(m, false, loadedLibs, function(err, extraLib) {
loadList = loadList.filter(function(e) {return e.module != extraLib.module} );
if(loadList.length == 0) {
var _imports = imports.join("");
var _defs = "\ndeclare global {\n" + defs.join("") + "\n}";
var libSource = _imports + _defs;
setTimeout(function() {
loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, file);
}, 500);
}
});
});
}
}
//#endregion
/*********** Create the monaco editor ***************/
var ed = monaco.editor.create(el, editorOptions);
//Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray.
try {
ed._standaloneKeybindingService.addDynamicKeybinding(
'-editor.action.insertLineAfter', // command ID prefixed by '-'
null, // keybinding
() => {} // need to pass an empty handler
);
} catch (error) { }
ed.nodered = {
refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh
}
//add f1 menu items for changing theme
for (var themeIdx = 0; themeIdx < monacoThemes.length; themeIdx++) {
var themeName = monacoThemes[themeIdx];
ed.addAction(createThemeMenuOption(themeName));
}
//#region "ACE compatability"
ed.selection = {};
ed.session = ed;
ed.renderer = {};
ed.setMode = function(mode, cb, resize) {
if(resize==null) { resize = true; }
mode = convertAceModeToMonacoLang(mode);
var oldModel = ed.getModel();
var oldValue = ed.getValue();
var newModel
if (oldModel) {
var oldScrollTop = ed.getScrollTop();
var oldScrollLeft = ed.getScrollLeft();
var oldSelections = ed.getSelections();
var oldPosition = ed.getPosition();
oldValue = oldModel.getValue() || "";
try {
if(!oldModel.isDisposed()) { oldModel.dispose(); }
} catch (error) { }
ed.setModel(null);
newModel = monaco.editor.createModel((oldValue || ""), mode);
ed.setModel(newModel);
ed.setScrollTop(oldScrollTop, 1/* immediate */);
ed.setScrollLeft(oldScrollLeft, 1/* immediate */);
ed.setPosition(oldPosition);
ed.setSelections(oldSelections);
} else {
newModel = monaco.editor.createModel((oldValue || ""), mode);
ed.setModel(newModel);
}
if (cb && typeof cb == "function") {
cb();
}
if(resize) {
this.resize(); //cause a call to layout()
}
}
ed.getRange = function getRange(){
var r = ed.getSelection();
r.start = {
row: r.selectionStartLineNumber-1,
column: r.selectionStartColumn-1
}
r.end = {
row: r.endLineNumber-1,
column: r.endColumn-1
}
return r;
}
ed.selection.getRange = ed.getRange;
ed.session.insert = function insert(position, text) {
//ACE position is zero based, monaco range is 1 based
var range = new monaco.Range(position.row+1, position.column+1, position.row+1, position.column+1);
var op = { range: range, text: text, forceMoveMarkers: true };
_executeEdits(this,[op]);
}
ed.setReadOnly = function setReadOnly(readOnly) {
ed.updateOptions({ readOnly: readOnly })
}
ed.session.replace = function replace(range, text) {
var op = { range: range, text: text, forceMoveMarkers: true };
_executeEdits(this,[op]);
}
function _executeEdits(editor, edits, endCursorState) {
var isReadOnly = editor.getOption(monaco.editor.EditorOptions.readOnly.id)
if (isReadOnly) {
editor.getModel().pushEditOperations(editor.getSelections(), edits, function() {
return endCursorState ? endCursorState : null;
});
} else {
editor.executeEdits("editor", edits);
}
}
ed.selectAll = function selectAll() {
const range = ed.getModel().getFullModelRange();
ed.setSelection(range);
}
ed.clearSelection = function clearSelection() {
ed.setPosition({column:1,lineNumber:1});
}
ed.getSelectedText = function getSelectedText() {
return ed.getModel().getValueInRange(ed.getSelection());
}
ed.insertSnippet = function insertSnippet(s) {
//https://github.com/microsoft/monaco-editor/issues/1112#issuecomment-429580604
//no great way of triggering snippets!
let contribution = ed.getContribution("snippetController2");
contribution.insert(s);
}
ed.destroy = function destroy() {
if(watchTimer) { clearInterval(watchTimer); }
try {
//dispose serverside addExtraLib disposible we added - this is the only way (https://github.com/microsoft/monaco-editor/issues/1002#issuecomment-564123586)
if(Object.keys(loadedLibs.JS).length) {
let entries = Object.entries(loadedLibs.JS);
for (let i = 0; i < entries.length; i++) {
try {
const entry = entries[i];
let name = entry[0];
loadedLibs.JS[name].dispose();
loadedLibs.JS[name] = null;
delete loadedLibs.JS[name];
} catch (error) { }
}
}
if(Object.keys(loadedLibs.TS).length) {
let entries = Object.entries(loadedLibs.TS);
for (let i = 0; i < entries.length; i++) {
try {
const entry = entries[i];
let name = entry[0];
loadedLibs.TS[name].dispose();
loadedLibs.TS[name] = null;
delete loadedLibs.TS[name];
} catch (error) { }
}
}
} catch (error) { }
try {
var m = this.getModel();
if(m && !m.isDisposed()) {
ed._initState = null;
m.dispose();
}
this.setModel(null);
} catch (e) { }
$(el).remove();
$(toolbarRow).remove();
}
ed.resize = function resize() {
ed.layout();
}
ed.renderer.updateFull = ed.resize.bind(ed);//call resize on ed.renderer.updateFull
ed.getSession = function getSession() {
return ed;
}
ed.getLength = function getLength() {
var _model = ed.getModel();
if (_model !== null) {
return _model.getLineCount();
}
return 0;
}
/**
* Scroll vertically as necessary and reveal a line.
* @param {Number} lineNumber
* @param {ScrollType|Number} [scrollType] Immediate: = 1, Smooth: = 0
*/
ed.scrollToLine = function scrollToLine(lineNumber, scrollType) {
ed.revealLine(lineNumber, scrollType);
}
ed.moveCursorTo = function moveCursorTo(lineNumber, colNumber) {
ed.setPosition({ lineNumber: lineNumber, column: colNumber });
}
// monaco.MarkerSeverity
// 1: "Hint"
// 2: "Info"
// 4: "Warning"
// 8: "Error"
// Hint: 1
// Info: 2
// Warning: 4
// Error: 8
ed.getAnnotations = function getAnnotations() {
let aceCompatibleMarkers;
try {
const _model = ed.getModel();
if (_model !== null) {
const id = _model.getLanguageId(); // e.g. javascript
const ra = _model.uri.authority; // e.g. model
const rp = _model.uri.path; // e.g. /18
const rs = _model.uri.scheme; // e.g. inmemory
const modelMarkers = monaco.editor.getModelMarkers(_model) || [];
const thisEditorsMarkers = modelMarkers.filter(function (marker) {
const _ra = marker.resource.authority; // e.g. model
const _rp = marker.resource.path; // e.g. /18
const _rs = marker.resource.scheme; // e.g. inmemory
return marker.owner == id && _ra === ra && _rp === rp && _rs === rs;
})
aceCompatibleMarkers = thisEditorsMarkers.map(function (marker) {
return {
row: marker.startLineNumber, //ACE compatible
column: marker.startColumn, //future
endColumn: marker.endColumn, //future
endRow: marker.endLineNumber, //future
text: marker.message,//ACE compatible
type: monaco.MarkerSeverity[marker.severity] ? monaco.MarkerSeverity[marker.severity].toLowerCase() : marker.severity //ACE compatible
}
})
}
} catch (error) {
console.log("Failed to get editor Annotations", error);
}
return aceCompatibleMarkers || [];
}
//ACE row and col are zero based. Add 1 for monaco lineNumber and column
ed.gotoLine = function gotoLine(row, col) {
ed.setPosition({ lineNumber: row+1, column: col+1 });
}
//ACE row and col are zero based. Minus 1 from monaco lineNumber and column
ed.getCursorPosition = function getCursorPosition() {
var p = ed.getPosition();
return { row: p.lineNumber-1, column: p.column-1 };
}
ed.setTheme = function(theme) {
monaco.editor.setTheme(theme);
userSelectedTheme = theme;//remember users choice for this session
}
ed.on = function (name, cb) {
switch (name) {
case "change":
case "input":
name = "onDidChangeModelContent";
break;
case "focus":
name = "onDidFocusEditorWidget";
break;
case "blur":
name = "onDidBlurEditorWidget";
break;
case "paste":
name = "onDidPaste";
break;
default:
break;
}
var fn;
if (ed[name]) {
fn = ed[name]
} else if (monaco.editor[name]) {
fn = monaco.editor[name];
} else {
console.warn("monaco - unknown event: " + name)
return;
}
fn(cb);
}
ed.getUndoManager = function getUndoManager() {
var o = {};
function isClean() {
try {
return ed.getModel().canUndo() === false
} catch (error) {
return false
}
}
o.isClean = isClean.bind(ed);
return o;
}
ed.setFontSize = function setFontSize(size) {
ed.updateOptions({ fontSize: size });
}
//#endregion "ACE compatability"
//final setup
ed.focusMemory = options.focus;
ed._mode = editorOptions.language;
//as models are signleton, consts and let are avialable to other javascript instances
//so when not focused, set editor mode to text temporarily to avoid multiple defs
if(editorOptions._language) {
ed._mode = editorOptions._language;
ed._tempMode = editorOptions.language;
}
ed.onDidBlurEditorWidget(function() {
ed.focusMemory = false;
ed.saveView();
if(isVisible(el) == false) {
onVisibilityChange(false, 0, el);
}
});
ed.onDidFocusEditorWidget(function() {
onVisibilityChange(true, 10, el);
});
function visibilityWatcher(element, callback) {
try {
var options = {
root: $(element).closest("div.red-ui-tray-content")[0] || document,
attributes: true,
childList: true,
};
var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
callback(entry.intersectionRatio > 0, 5, entry.target);
});
}, options);
observer.observe(element);
} catch (e1) {
//browser not supporting IntersectionObserver? then fall back to polling!
try {
let elVisibleMem = isVisible(el)
watchTimer = setInterval(function() {
let elVisible = isVisible(el);
if(elVisible != elVisibleMem) {
callback(elVisible, 5, element);
}
elVisibleMem = elVisible;
}, 100);
} catch (e2) { }
}
}
function onVisibilityChange(visible, delay, element) {
delay = delay || 50;
if (visible) {
if (ed.focusMemory) {
setTimeout(function () {
if (element.parentElement) { //ensure el is still in DOM
ed.focus();
}
}, 300)
}
if (ed._initState) {
setTimeout(function () {
if (element.parentElement) { //ensure el is still in DOM
ed.restoreViewState(ed._initState);
ed._initState = null;
}
}, delay);
}
if (ed._mode == "javascript" && ed._tempMode == "text") {
ed._tempMode = "";
setTimeout(function () {
if (element.parentElement) { //ensure el is still in DOM
ed.setMode('javascript', undefined, false);
}
}, delay);
}
} else if (ed._mode == "javascript" && ed._tempMode != "text") {
if (element.parentElement) { //ensure el is still in DOM
ed.setMode('text', undefined, false);
ed._tempMode = "text";
}
}
}
visibilityWatcher(el, onVisibilityChange);
if (editorOptions.language === 'markdown') {
$(el).addClass("red-ui-editor-text-container-toolbar");
ed.toolbar = RED.editor.customEditTypes['_markdown'].buildToolbar(toolbarRow, ed);
if (options.expandable !== false) {
var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(ed.toolbar);
RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
expandButton.on("click", function (e) {
e.preventDefault();
var value = ed.getValue();
ed.saveView();
RED.editor.editMarkdown({
value: value,
width: "Infinity",
stateId: options.stateId,
cancel: function () {
ed.focus();
},
complete: function (v, cursor) {
ed.setValue(v, -1);
setTimeout(function () {
ed.focus();
ed.restoreView();
}, 300);
}
})
});
}
var helpButton = $('<button type="button" class="red-ui-editor-text-help red-ui-button red-ui-button-small"><i class="fa fa-question"></i></button>').appendTo($(el).parent());
RED.popover.create({
target: helpButton,
trigger: 'click',
size: "small",
direction: "left",
content: RED._("markdownEditor.format"),
autoClose: 50
});
}
ed.getView = function () {
return ed.saveViewState();
}
ed.saveView = function (debuginfo) {
if (!options.stateId) { return; } //only possible if created with a unique stateId
window._editorStateMonaco = window._editorStateMonaco || {};
var state = ed.getView();
window._editorStateMonaco[options.stateId] = state;
return state;
}
ed.restoreView = function (state) {
if (!options.stateId) { return; } //only possible if created with a unique stateId
window._editorStateMonaco = window._editorStateMonaco || {};
var _state = state || window._editorStateMonaco[options.stateId];
if (!_state) { return; } //no view state available
try {
if (ed.type) { //is editor already initialised?
ed.restoreViewState(_state);
} else {
ed._initState = _state;
}
} catch (error) {
delete window._editorStateMonaco[options.stateId];
}
};
ed.restoreView();
if (options.cursor && !ed._initState) {
var row = options.cursor.row || options.cursor.lineNumber;
var col = options.cursor.column || options.cursor.col;
ed.gotoLine(row, col);
}
ed.type = type;
return ed;
}
function isVisible(el) {
if(!el.offsetHeight && !el.offsetWidth) { return false; }
if(getComputedStyle(el).visibility === 'hidden') { return false; }
return true;
}
return {
/**
* Editor type
* @memberof RED.editor.codeEditor.monaco
*/
get type() { return type; },
/**
* Editor initialised
* @memberof RED.editor.codeEditor.monaco
*/
get initialised() { return initialised; },
/**
* Initialise code editor
* @param {object} options - initialisation options
* @memberof RED.editor.codeEditor.monaco
*/
init: init,
/**
* Create a code editor
* @param {object} options - the editor options
* @memberof RED.editor.codeEditor.monaco
*/
create: create
}
})();