diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index 90a0b450fb..494d7c2052 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -73,6 +73,8 @@ import { EditorContainer, EditorHolder } from './MobileEditor'; import { FolderIcon } from '../../../../common/icons'; import IconButton from '../../../../common/IconButton'; +import contextAwareHinter from '../contextAwareHinter'; + emmet(CodeMirror); window.JSHINT = JSHINT; @@ -465,11 +467,8 @@ class Editor extends React.Component { () => { const c = _cm.getCursor(); const token = _cm.getTokenAt(c); - - const hints = this.hinter - .search(token.string) - .filter((h) => h.item.text[0] === token.string[0]); - + const hints = contextAwareHinter(_cm, { hinter: this.hinter }); + console.log('hints= ', hints); return { list: hints, from: CodeMirror.Pos(c.line, token.start), @@ -478,6 +477,26 @@ class Editor extends React.Component { }, hintOptions ); + + // CodeMirror.showHint( + // _cm, + // () => { + // const c = _cm.getCursor(); + // const token = _cm.getTokenAt(c); + // const hints = this.hinter + // .search(token.string) + // .filter((h) => h.item.text[0] === token.string[0]); + // console.log('c= ', c); + // console.log('token= ', token); + // console.log('hints= ', hints); + // return { + // list: hints, + // from: CodeMirror.Pos(c.line, token.start), + // to: CodeMirror.Pos(c.line, c.ch) + // }; + // }, + // hintOptions + // ); } else if (_cm.options.mode === 'css') { // CSS CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions); diff --git a/client/modules/IDE/components/contextAwareHinter.js b/client/modules/IDE/components/contextAwareHinter.js new file mode 100644 index 0000000000..ac4ca6452c --- /dev/null +++ b/client/modules/IDE/components/contextAwareHinter.js @@ -0,0 +1,100 @@ +import CodeMirror from 'codemirror'; +import parseCode from './parseCode'; + +const scopeMap = require('./finalScopeMap.json'); + +export default function contextAwareHinter(cm, options = {}) { + const { hinter } = options; + if (!hinter || typeof hinter.search !== 'function') { + console.warn('Hinter is not available or invalid.'); + return { list: [], from: cm.getCursor(), to: cm.getCursor() }; + } + + const { line, ch } = cm.getCursor(); // getCursor has line, ch, sticky + console.log('cm.getcursor ', cm.getCursor()); + const { start, end, string } = cm.getTokenAt({ line, ch }); + // console.log('cm.gettokenat', cm.getTokenAt()); + const currentWord = string.trim(); + console.log('currentwork ', currentWord); + + const context = parseCode(cm); // e.g. 'draw' + const allHints = hinter.search(currentWord); // <- from options, not cm.hinter + const whitelist = scopeMap[context]?.whitelist || []; + const blacklist = scopeMap[context]?.blacklist || []; + console.log('allhints: ', allHints); + + // for each hint, only keep ones that match the typed prefix + const filteredHints = allHints + .filter( + (h) => + h && + h.item && + typeof h.item.text === 'string' && + h.item.text.startsWith(currentWord) + ) + .map((h) => { + const name = h.item.text; + const isWhitelisted = whitelist.includes(name); + const isBlacklisted = blacklist.includes(name); + + const from = CodeMirror.Pos(line, start); + const to = CodeMirror.Pos(line, end); + + let className = ''; + if (isBlacklisted) { + className = 'blacklisted-hint'; + } else if (isWhitelisted) { + className = 'whitelisted-hint'; + } + + return { + text: name, // Ensure `text` is explicitly defined + displayText: h.item.displayText || name, + className, + from, + to, + render: (el, self, data) => { + el.innerText = data.text; + if (isBlacklisted) { + el.style.color = 'red'; + el.style.opacity = 0.6; + } else if (isWhitelisted) { + el.style.fontWeight = 'bold'; + } + }, + hint: (editor, data, completion) => { + const { from: fromPos, to: toPos } = completion; + + if (!completion.text || typeof completion.text !== 'string') { + console.error('Invalid completion.text:', completion); + return; + } + + editor.replaceRange(completion.text, fromPos, toPos); + + if (blacklist.includes(completion.text)) { + console.warn( + `Using "${completion.text}" inside "${context}" is not recommended.` + ); + } + } + }; + }); + + console.log('filtered hints: ', filteredHints); + + const sorted = filteredHints.sort((a, b) => { + const score = (name) => { + if (whitelist.includes(name)) return 0; + if (blacklist.includes(name)) return 2; + return 1; + }; + return score(a.text) - score(b.text) || a.text.localeCompare(b.text); + }); + + return { + list: sorted, + from: CodeMirror.Pos(line, start), + to: CodeMirror.Pos(line, end) + }; +} diff --git a/client/modules/IDE/components/finalScopeMap.json b/client/modules/IDE/components/finalScopeMap.json new file mode 100644 index 0000000000..9b6c09abde --- /dev/null +++ b/client/modules/IDE/components/finalScopeMap.json @@ -0,0 +1,474 @@ +{ + "setup": { + "whitelist": [ + "acos", + "alpha", + "angleMode", + "append", + "arc", + "asin", + "atan", + "background", + "baseColorShader", + "beginClip", + "beginContour", + "beginGeometry", + "beginShape", + "bezier", + "bezierDetail", + "bezierPoint", + "bezierTangent", + "bezierVertex", + "blend", + "blendMode", + "blue", + "boolean", + "brightness", + "buildGeometry", + "byte", + "ceil", + "changed", + "char", + "circle", + "class", + "clip", + "color", + "colorMode", + "concat", + "cone", + "copy", + "cos", + "createA", + "createAudio", + "createButton", + "createCamera", + "createCanvas", + "createCapture", + "createCheckbox", + "createColorPicker", + "createDiv", + "createElement", + "createFileInput", + "createFilterShader", + "createFramebuffer", + "createGraphics", + "createImage", + "createImg", + "createInput", + "createModel", + "createNumberDict", + "createP", + "createRadio", + "createSelect", + "createShader", + "createSlider", + "createSpan", + "createStringDict", + "createVector", + "createVideo", + "curve", + "curveDetail", + "curvePoint", + "curveTangent", + "curveVertex", + "day", + "debugMode", + "degrees", + "describe", + "describeElement", + "dist", + "ellipse", + "ellipseMode", + "endClip", + "endContour", + "endGeometry", + "endShape", + "erase", + "exp", + "fill", + "filter", + "float", + "floor", + "fract", + "frameRate", + "freeGeometry", + "get", + "getAudioContext", + "getURL", + "getURLParams", + "getURLPath", + "green", + "gridOutput", + "hex", + "hour", + "hue", + "image", + "imageMode", + "input", + "int", + "join", + "lerp", + "lerpColor", + "lightness", + "lights", + "line", + "loadFont", + "loadImage", + "loadPixels", + "log", + "loop", + "mag", + "map", + "match", + "matchAll", + "max", + "millis", + "min", + "minute", + "model", + "month", + "mousePressed", + "nf", + "nfc", + "nfp", + "nfs", + "noCanvas", + "noCursor", + "noErase", + "noFill", + "noLoop", + "noSmooth", + "noStroke", + "noTint", + "noise", + "noiseDetail", + "noiseSeed", + "pixelDensity", + "plane", + "point", + "pop", + "pow", + "print", + "push", + "quad", + "quadraticVertex", + "radians", + "random", + "randomSeed", + "rect", + "rectMode", + "red", + "reverse", + "rotate", + "rotateX", + "rotateY", + "round", + "saturation", + "saveCanvas", + "scale", + "second", + "select", + "selectAll", + "set", + "setAttributes", + "setCamera", + "setMoveThreshold", + "setShakeThreshold", + "shader", + "shorten", + "shuffle", + "sin", + "sort", + "splice", + "split", + "splitTokens", + "sq", + "sqrt", + "square", + "storeItem", + "str", + "stroke", + "strokeCap", + "strokeJoin", + "strokeWeight", + "subset", + "tan", + "text", + "textAlign", + "textAscent", + "textDescent", + "textFont", + "textLeading", + "textOutput", + "textSize", + "textStyle", + "textWidth", + "textWrap", + "tint", + "torus", + "translate", + "triangle", + "trim", + "unchar", + "unhex", + "updatePixels", + "vertex", + "year" + ], + "blacklist":[ + "draw", + "setup", + "preload" + ] + }, + "draw": { + "whitelist": [ + "abs", + "ambientLight", + "ambientMaterial", + "applyMatrix", + "arc", + "atan2", + "background", + "beginClip", + "beginContour", + "beginShape", + "bezier", + "bezierPoint", + "bezierVertex", + "box", + "camera", + "circle", + "clear", + "clearDepth", + "clip", + "color", + "cone", + "constrain", + "cos", + "createVector", + "cursor", + "curve", + "curvePoint", + "curveTightness", + "curveVertex", + "cylinder", + "describe", + "directionalLight", + "ellipse", + "ellipsoid", + "emissiveMaterial", + "endClip", + "endContour", + "endShape", + "exp", + "fill", + "filter", + "frameRate", + "frustum", + "getAudioContext", + "getItem", + "getTargetFrameRate", + "gridOutput", + "image", + "imageLight", + "keyIsDown", + "lerp", + "lightFalloff", + "lights", + "line", + "log", + "map", + "metalness", + "millis", + "model", + "noFill", + "noLights", + "noLoop", + "noStroke", + "noise", + "norm", + "normal", + "normalMaterial", + "orbitControl", + "ortho", + "paletteLerp", + "panorama", + "perspective", + "plane", + "point", + "pointLight", + "pop", + "push", + "quad", + "quadraticVertex", + "radians", + "random", + "randomGaussian", + "rect", + "resetMatrix", + "resetShader", + "rotate", + "rotateX", + "rotateY", + "rotateZ", + "scale", + "shader", + "shearX", + "shearY", + "shininess", + "sin", + "specularColor", + "specularMaterial", + "sphere", + "spotLight", + "sq", + "sqrt", + "square", + "stroke", + "strokeWeight", + "tan", + "text", + "textAlign", + "textFont", + "textOutput", + "textSize", + "texture", + "textureMode", + "textureWrap", + "tint", + "torus", + "translate", + "triangle", + "vertex" + ], + "blacklist":[ + "createCanvas", "let","const", "var", "loadJSON", "loadStrings","loadShader","loadTable","loadXML","save","draw", "setup", "preload" + ] + }, + "global": { + "whitelist": [ + "arrayCopy", + "createCanvas", + "createElement", + "createGraphics", + "createImage", + "describe", + "print", + "random", + "save", + "text", + "textAlign", + "textSize", + "let" + ] + }, + "mousePressed": { + "whitelist": [ + "background", + "circle", + "clear", + "displayDensity", + "dist", + "fill", + "fullscreen", + "httpPost", + "image", + "noStroke", + "pixelDensity", + "removeElements", + "resizeCanvas", + "saveFrames", + "setAttributes", + "stroke", + "strokeWeight", + "text", + "userStartAudio" + ] + }, + "preload": { + "whitelist": [ + "createConvolver", + "createVideo", + "httpDo", + "httpGet", + "loadBytes", + "loadFont", + "loadImage", + "loadJSON", + "loadModel", + "loadShader", + "loadSound", + "loadStrings", + "loadTable", + "loadXML", + "soundFormats" + ] + }, + "doubleClicked": { + "whitelist": [ + "clearStorage", + "createWriter", + "dist", + "exitPointerLock", + "fill", + "image", + "isLooping", + "linePerspective", + "loop", + "noDebugMode", + "noLoop", + "print", + "redraw", + "remove", + "removeElements", + "removeItem", + "requestPointerLock", + "resizeCanvas", + "saveJSON", + "saveStrings", + "setCamera" + ] + }, + "mouseReleased": { + "whitelist": [ + "fill", + "noStroke", + "setAttributes", + "stroke" + ] + }, + "mouseClicked": { + "whitelist": [ + "fill", + "strokeWeight" + ] + }, + "windowResized": { + "whitelist": [ + "resizeCanvas" + ] + }, + "keyPressed": { + "whitelist": [ + "saveFrames", + "saveGif" + ] + }, + "deviceMoved": { + "whitelist": [ + "setMoveThreshold", + "setShakeThreshold" + ] + }, + "touchStarted": { + "whitelist": [ + "random" + ] + }, + "touchEnded": { + "whitelist": [ + "random" + ] + } +} \ No newline at end of file diff --git a/client/modules/IDE/components/parseCode.js b/client/modules/IDE/components/parseCode.js new file mode 100644 index 0000000000..dc066b9ae2 --- /dev/null +++ b/client/modules/IDE/components/parseCode.js @@ -0,0 +1,47 @@ +const acorn = require('acorn'); +const walk = require('acorn-walk'); + +export default function parseCode(_cm) { + const code = _cm.getValue(); + const cursor = _cm.getCursor(); + const offset = _cm.indexFromPos(cursor); + + let ast; + try { + ast = acorn.parse(code, { + ecmaVersion: 'latest', + sourceType: 'script' + }); + } catch (e) { + console.warn('Failed to parse code', e.message); + return 'global'; + } + + let context = 'global'; + + walk.fullAncestor(ast, (node, ancestors) => { + if ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + if (offset >= node.start && offset <= node.end) { + if (node.id && node.id.name) { + context = node.id.name; + } else { + const parent = ancestors[ancestors.length - 2]; + if ( + parent?.type === 'VariableDeclarator' && + parent.id.type === 'Identifier' + ) { + context = parent.id.name; + } else { + context = '(anonymous)'; + } + } + } + } + }); + + return context; +} diff --git a/client/modules/IDE/components/parseCodeBabel.js b/client/modules/IDE/components/parseCodeBabel.js new file mode 100644 index 0000000000..14e50d2e14 --- /dev/null +++ b/client/modules/IDE/components/parseCodeBabel.js @@ -0,0 +1,46 @@ +// parseCodeWithBabel.js +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; + +export default function parseCode(_cm) { + const code = _cm.getValue(); + const cursor = _cm.getCursor(); + const offset = _cm.indexFromPos(cursor); + + let ast; + try { + ast = parser.parse(code, { + sourceType: 'script', + plugins: ['jsx', 'typescript'] // add plugins as needed + }); + } catch (e) { + console.warn('Failed to parse code with Babel:', e.message); + return 'global'; + } + + let context = 'global'; + + traverse(ast, { + Function(path) { + const { node } = path; + if (offset >= node.start && offset <= node.end) { + if (node.id && node.id.name) { + context = node.id.name; + } else { + const parent = path.parentPath.node; + if ( + parent.type === 'VariableDeclarator' && + parent.id.type === 'Identifier' + ) { + context = parent.id.name; + } else { + context = '(anonymous)'; + } + } + path.stop(); // Stop traversal once we found the function context + } + } + }); + + return context; +} diff --git a/client/modules/IDE/components/show-hint.js b/client/modules/IDE/components/show-hint.js index 99f7552006..ed08af3e1e 100644 --- a/client/modules/IDE/components/show-hint.js +++ b/client/modules/IDE/components/show-hint.js @@ -7,6 +7,12 @@ // declare global: DOMRect +// The first function (mod) is a wrapper to support different JavaScript environments. +// The second function (inside) contains the actual implementation. +import parseCode from './parseCode'; +import CodeMirror from 'codemirror'; +import warnIfBlacklisted from './warn'; + (function (mod) { if (typeof exports == 'object' && typeof module == 'object') // CommonJS @@ -16,6 +22,8 @@ define(['codemirror'], mod); // Plain browser env else mod(CodeMirror); + + // This part uptill here makes the code compatible with multiple JavaScript environments, so it can run in different places })(function (CodeMirror) { 'use strict'; @@ -25,16 +33,24 @@ // This is the old interface, kept around for now to stay // backwards-compatible. CodeMirror.showHint = function (cm, getHints, options) { - if (!getHints) return cm.showHint(options); + console.log('showhint was called: ', cm, getHints, options); + if (!getHints) return cm.showHint(options); // if not getHints function passed, it assumes youre using the newer interface + // restructured options to call the new c.showHint() method if (options && options.async) getHints.async = true; var newOpts = { hint: getHints }; if (options) for (var prop in options) newOpts[prop] = options[prop]; + console.log('newopts: ', newOpts); + const context = parseCode(cm); + console.log('Cursor context =', context); return cm.showHint(newOpts); }; + // this adds a method called showHint to every cm editor instance (editor.showHint()) CodeMirror.defineExtension('showHint', function (options) { options = parseOptions(this, this.getCursor('start'), options); + console.log('options are: '); var selections = this.listSelections(); + console.log('selections are: ', selections); if (selections.length > 1) return; // By default, don't allow completion when something is selected. // A hint function can have a `supportsSelection` property to @@ -42,18 +58,20 @@ if (this.somethingSelected()) { if (!options.hint.supportsSelection) return; // Don't try with cross-line selections + // if selection spans multiple lines, bail out for (var i = 0; i < selections.length; i++) if (selections[i].head.line != selections[i].anchor.line) return; } - if (this.state.completionActive) this.state.completionActive.close(); + if (this.state.completionActive) this.state.completionActive.close(); // close an already active autocomplete session if active + // create a new completion object and saves it to this.state.completionActive var completion = (this.state.completionActive = new Completion( this, options )); - if (!completion.options.hint) return; + if (!completion.options.hint) return; // safety check to ensure hint is valid - CodeMirror.signal(this, 'startCompletion', this); + CodeMirror.signal(this, 'startCompletion', this); // emits a signal; fires a startCompletion event on editor instance completion.update(true); }); @@ -61,19 +79,22 @@ if (this.state.completionActive) this.state.completionActive.close(); }); + // defines a constructor function function Completion(cm, options) { this.cm = cm; this.options = options; - this.widget = null; + this.widget = null; // will hold a reference to the dropdown menu that shows suggestions this.debounce = 0; this.tick = 0; - this.startPos = this.cm.getCursor('start'); + this.startPos = this.cm.getCursor('start'); // startPos is a {line,ch} object used to remember where hinting started + // startLen is the len of the line minus length of any selected text this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; if (this.options.updateOnCursorActivity) { - var self = this; + var self = this; // stores ref to this as self so it can be accessed inside the nested function + // adds an event listener to the editor; called when the cursor moves cm.on( 'cursorActivity', (this.activityFunc = function () { @@ -95,12 +116,14 @@ if (!this.active()) return; this.cm.state.completionActive = null; this.tick = null; + // removes the current activity listener if (this.options.updateOnCursorActivity) { this.cm.off('cursorActivity', this.activityFunc); } - + // signals and removes the widget if (this.widget && this.data) CodeMirror.signal(this.data, 'close'); if (this.widget) this.widget.close(); + // emits a completition end event CodeMirror.signal(this.cm, 'endCompletion', this.cm); }, @@ -109,26 +132,40 @@ }, pick: function (data, i) { + // selects an item from the suggestion list + console.log('data, i= ', data, i); var completion = data.list[i], self = this; + this.cm.operation(function () { - if (completion.hint) completion.hint(self.cm, data, completion); - else + // this is how cm allows custom behavior per suggestion + // if hint is provided on a hint object, it will be called instead of the default replace range + const name = completion.item?.text; + if (name) warnIfBlacklisted(self.cm, name); + + if (completion.hint) { + completion.hint(self.cm, data, completion); + } else { + console.log('gettext(C)= ', getText(completion)); self.cm.replaceRange( getText(completion), completion.from || data.from, completion.to || data.to, 'complete' ); + } + // signals that a hint was picked and scrolls to it CodeMirror.signal(data, 'pick', completion); self.cm.scrollIntoView(); }); + // closes widget if closeOnPick is enabled if (this.options.closeOnPick) { this.close(); } }, cursorActivity: function () { + // if a debounce is scheduled, cancel it to avoid outdated updates if (this.debounce) { cancelAnimationFrame(this.debounce); this.debounce = 0; @@ -149,6 +186,7 @@ !pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)) ) { + console.log('this.close called'); this.close(); } else { var self = this; @@ -192,6 +230,7 @@ function parseOptions(cm, pos, options) { var editor = cm.options.hintOptions; var out = {}; + // copies all default hint settings into out for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; if (editor) for (var prop in editor) @@ -200,14 +239,17 @@ for (var prop in options) if (options[prop] !== undefined) out[prop] = options[prop]; if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos); + console.log('out is ', out); return out; } - + // extracts the visible text from a completion entry function getText(completion) { + console.log('gettext called'); if (typeof completion === 'string') return completion; else return completion.item.text; } + // builds a key mapping object to define keyboard behavior for autocomplete function buildKeyMap(completion, handle) { var baseMap = { Up: function () { @@ -232,7 +274,7 @@ Tab: handle.pick, Esc: handle.close }; - + // checks if the user is on macOS and adds shortcuts accordingly var mac = /Mac/.test(navigator.platform); if (mac) { @@ -244,6 +286,7 @@ }; } + // user defined custom key bindings var custom = completion.options.customKeys; var ourMap = custom ? {} : baseMap; function addBinding(key, val) { @@ -257,6 +300,7 @@ else bound = val; ourMap[key] = bound; } + // apply all custom key bindings and extraKeys if (custom) for (var key in custom) if (custom.hasOwnProperty(key)) addBinding(key, custom[key]); @@ -267,15 +311,20 @@ return ourMap; } + // hintsElement is the parent for hints and el is the clicked element within that container function getHintElement(hintsElement, el) { + console.log('el is ', el); while (el && el != hintsElement) { - if (el.nodeName.toUpperCase() === 'LI' && el.parentNode == hintsElement) + if (el.nodeName.toUpperCase() === 'LI' && el.parentNode == hintsElement) { + console.log('new el is ', el); return el; + } el = el.parentNode; } } function displayHint(name, type, p5) { + console.log('name is', name, type, p5); return `

\ ${name}\ , \ @@ -292,8 +341,12 @@ ${ }

`; } - function getInlineHintSuggestion(focus, tokenLength) { + function getInlineHintSuggestion(cm, focus, tokenLength) { + const name = focus.item?.text; + console.log('the focus is: ', focus, name); + if (name) warnIfBlacklisted(cm, name); const suggestionItem = focus.item; + // builds the remainder of the suggestion excluding what user already typed const baseCompletion = `${suggestionItem.text.slice( tokenLength )}`; @@ -310,6 +363,7 @@ ${ ); } + // clears existing inline hint (like the part is suggested) function removeInlineHint(cm) { if (cm.state.inlineHint) { cm.state.inlineHint.clear(); @@ -326,6 +380,7 @@ ${ if (token && focus.item) { const suggestionHTML = getInlineHintSuggestion( + cm, focus, token.string.length ); @@ -341,7 +396,12 @@ ${ } } + // defines the autocomplete dropdown ui; renders the suggestions + // completion = the autocomplete context having cm and options + // data = object with the list of suggestions function Widget(completion, data) { + console.log('widget completetition= ', completion); + console.log('widget data= ', data); this.id = 'cm-complete-' + Math.floor(Math.random(1e6)); this.completion = completion; this.data = data; @@ -365,7 +425,6 @@ ${ changeInlineHint(cm, data.list[this.selectedHint]); var completions = data.list; - for (var i = 0; i < completions.length; ++i) { var elt = hints.appendChild(ownerDocument.createElement('li')), cur = completions[i]; @@ -383,6 +442,7 @@ ${ const name = getText(cur); if (cur.item && cur.item.type) { + console.log('display hint calllllled'); cur.displayText = displayHint(name, cur.item.type, cur.item.p5); } diff --git a/client/modules/IDE/components/warn.js b/client/modules/IDE/components/warn.js new file mode 100644 index 0000000000..78ffe5e7a4 --- /dev/null +++ b/client/modules/IDE/components/warn.js @@ -0,0 +1,19 @@ +import parseCode from './parseCode'; + +const scopeMap = require('./finalScopeMap.json'); + +/** + * Checks if a completion is blacklisted in the current context and logs a warning if so. + * @param {CodeMirror.Editor} cm - The CodeMirror instance + * @param {string} text - The name of the selected function + */ +export default function warnIfBlacklisted(cm, text) { + const context = parseCode(cm); + const blacklist = scopeMap[context]?.blacklist || []; + + if (blacklist.includes(text)) { + console.warn( + `⚠️ Function "${text}" is usually not used in "${context}" context. Please be careful.` + ); + } +}