changed formula editor to use codeeditor, added support for object autocomplete in the code editor

This commit is contained in:
Alexandru Stefan committed 2021-11-10 16:18:11 +02:00
1 parent 8fe28e68f9
commit 01838ceecc
6 files changed
+130 -262

No files matched your search

@@ -6,7 +6,6 @@ function newFoundationsCodeEditorSpace() {
container: undefined,
editorPage: undefined,
openSpaceArea: openSpaceArea,
// navigateTo: navigateTo,
physics: physics,
draw: draw,
getContainer: getContainer,
@@ -66,7 +66,7 @@ function newFoundationsCodeEditorEditorPage() {
monacoEditor = undefined
}
function render(originatingNode, rootNodes) {
function render(originatingNode) {
thisObject.originatingNode = originatingNode
intellisenseModels = []
@@ -93,6 +93,9 @@ function newFoundationsCodeEditorEditorPage() {
monaco.editor.createModel(codeString, "javascript")
monaco.editor.createModel(recordFormulaString, "javascript")
showAutocompletionFor(tradingEngineObj)
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
@@ -109,70 +112,145 @@ function newFoundationsCodeEditorEditorPage() {
monacoEditor.getModel().onDidChangeContent(function () {
originatingNode.payload.node.code = monacoEditor.getValue()
})
function showAutocompletionFor(obj) {
// Disable default autocompletion for javascript
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true });
// Helper function to return the monaco completion item type of a thing
function getType(thing, isMember) {
isMember = (isMember === undefined) ? (typeof isMember == "boolean") ? isMember : false : false; // Give isMember a default value of false
switch ((typeof thing).toLowerCase()) {
case "object":
return monaco.languages.CompletionItemKind.Class;
case "function":
return (isMember) ? monaco.languages.CompletionItemKind.Method : monaco.languages.CompletionItemKind.Function;
default:
return (isMember) ? monaco.languages.CompletionItemKind.Property : monaco.languages.CompletionItemKind.letiable;
}
}
// Register object that will return autocomplete items
monaco.languages.registerCompletionItemProvider('javascript', {
// Run this function when the period or open parenthesis is typed (and anything after a space)
triggerCharacters: ['.', '('],
// Function to generate autocompletion results
provideCompletionItems: function(model, position, token) {
// Split everything the user has typed on the current line up at each space, and only look at the last word
let last_chars = model.getValueInRange({startLineNumber: position.lineNumber, startColumn: 0, endLineNumber: position.lineNumber, endColumn: position.column});
let words = last_chars.replace("\t", "").split(" ");
let active_typing = words[words.length - 1]; // What the user is currently typing (everything after the last space)
if (active_typing.includes('(')) {
let split = active_typing.split('(');
active_typing = split[split.length-1]
}
// If the last character typed is a period then we need to look at member objects of the obj object
let is_member = active_typing.charAt(active_typing.length - 1) === ".";
// Array of autocompletion results
let result = [];
// Used for generic handling between member and non-member objects
let last_token = obj;
let prefix = '';
if (is_member) {
// Is a member, get a list of all members, and the prefix
let parents = active_typing.substring(0, active_typing.length - 1).split(".");
last_token = obj[parents[0]];
prefix = parents[0];
// Loop through all the parents the current one will have (to generate prefix)
for (let i = 1; i < parents.length; i++) {
if (last_token.hasOwnProperty(parents[i])) {
prefix += '.' + parents[i];
last_token = last_token[parents[i]];
} else {
// Not valid
return result;
}
}
prefix += '.';
}
// Get all the child properties of the last token
for (let prop in last_token) {
// Do not show properites that begin with "__"
if (last_token.hasOwnProperty(prop) && !prop.startsWith("__")) {
// Get the detail type (try-catch) incase object does not have prototype
let details = '';
try {
details = last_token[prop].__proto__.constructor.name;
} catch (e) {
details = typeof last_token[prop];
}
// Create completion object
let to_push = {
label: prefix + prop,
kind: getType(last_token[prop], is_member),
detail: details,
insertText: prop
};
// Change insertText and documentation for functions
if (to_push.detail.toLowerCase() === 'function') {
to_push.insertText += "(";
to_push.documentation = (last_token[prop].toString()).split("{")[0]; // Show function prototype in the documentation popup
}
// Add to final results
result.push(to_push);
}
}
return {
suggestions: result
};
}
});
}
});
}
/**
* The autocomplete models will be based on:
* 1. all data mines loaded into workspace
* 2. trading engine loaded into the workspace
* 1. all data mines loaded into workspace - by creating a model in monaco (basically is like opening a file containing all the variables in vs-code, thus it will make them available in other files as well)
* 2. trading engine loaded into the workspace - we create an obj. representation using getProtocolNode and we are using monaco registerCompletionItemProvider
*
* TODO: It is compulsory that at one moment in time we should add proper definitions for our variables (to mimic some framework), but this is a different task and can be improved over time
* TODO: It is recommended that for extending it to read the monaco editor docs: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html and https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
* TODO: Right now the intellisense model is rather primitive being built on existing code available in the workspace without having any definitions, but strong enough to provide accurate autocompletion
*/
function buildIntelliSenseModels() {
UI.projects.foundations.spaces.designSpace.workspace.workspaceNode.rootNodes.forEach(node => {
if (node.type === 'Data Mine') {
//Getting all the Javascript code into one single variable
//Getting all the Javascript code into one single letiable
UI.projects.visualScripting.utilities.branches.nodeBranchToArray(node, 'Procedure Javascript Code').forEach(node => {
codeString += '\n'
codeString += node.code
})
//Getting all the Formula code into one single variable
//Getting all the Formula code into one single letiable
UI.projects.visualScripting.utilities.branches.nodeBranchToArray(node, 'Record Formula').forEach(node => {
recordFormulaString += '\n'
recordFormulaString += node.code
})
} else if (node.type === 'Trading Engine') {
// Create an object representation for
tradingEngineObj = UI.projects.visualScripting.functionLibraries.protocolNode.getProtocolNode(node, false, true, true, false, false, undefined)
tradingEngineObj = {tradingEngine : UI.projects.visualScripting.functionLibraries.protocolNode.getProtocolNode(node, false, false, false, false, false, undefined)}
}
})
console.log(codeString)
console.log(recordFormulaString)
function extractCodeValues(node) {
}
function stepBackUntilNodeType(originatingNode, nodeType) {
if (originatingNode === undefined) {
return
}
if (nodeType === undefined) {
return
}
if (originatingNode.payload === undefined) {
return
}
if (originatingNode.payload.parentNode === undefined) {
return
}
let parent = originatingNode.payload.parentNode;
if (parent.type === nodeType) {
console.log(parent)
return parent
} else {
stepBackUntilNodeType(parent, nodeType)
}
}
}
function createCodePath() {
let parentNodeStructure = []
@@ -202,5 +280,4 @@ function newFoundationsCodeEditorEditorPage() {
}
}
}
}
@@ -1,6 +1,7 @@
/**
*
* The only reason we are keeping this class is to keep consistency across the uiObject implementation
* It will also provide a good place to enrich the action received, separating the logic of the action from the editor space
* Entire functionality is provided by CodeEditor Space
*/
function newCodeEditor() {
@@ -1,200 +1,30 @@
/**
*
* The only reason we are keeping this class is to keep consistency across the uiObject implementation when it comes to action handlers
* It will also provide a good place to enrich the action received, separating the logic of the action from the editor space
* Entire functionality is provided by CodeEditor Space
*/
function newFormulaEditor() {
const MODULE_NAME = 'Formula Editor'
let thisObject = {
isVisibleFunction: undefined,
visible: false,
imagePathOK: undefined,
imagePathNOT_OK: undefined,
rawRadius: undefined,
targetRadius: undefined,
currentRadius: undefined,
container: undefined,
payload: undefined,
deactivate: deactivate,
activate: activate,
physics: physics,
drawBackground: drawBackground,
drawForeground: drawForeground,
getContainer: getContainer,
finalize: finalize,
initialize: initialize
}
thisObject.container = newContainer()
thisObject.container.initialize(MODULE_NAME)
thisObject.container.isClickeable = false
thisObject.container.isDraggeable = false
thisObject.container.detectMouseOver = false
thisObject.container.frame.radius = 0
thisObject.container.frame.position.x = 0
thisObject.container.frame.position.y = 0
let isMouseOver = false
let SIZE_FACTOR = 1.0
return thisObject
function finalize() {
thisObject.container.finalize()
thisObject.container = undefined
thisObject.payload = undefined
thisObject.isVisibleFunction = undefined
}
function initialize() {
}
function deactivate() {
if (thisObject.visible === true) {
thisObject.visible = false
let textArea = document.getElementById('textArea')
textArea.style.display = 'none'
thisObject.payload.node.code = textArea.value
}
EDITOR_ON_FOCUS = false
}
function activate(action) {
thisObject.visible = true
thisObject.payload = action.node.payload
thisObject.rawRadius = 8
thisObject.targetRadius = thisObject.container.frame.radius
thisObject.currentRadius = 0
thisObject.payload.uiObject.resetErrorMessage()
thisObject.payload.uiObject.uiObjectTitle.exitEditMode()
let textArea = document.getElementById('textArea')
textArea.value = thisObject.payload.node.code
textArea.style = 'resize: none;' +
' border: none;' +
' outline: none;' +
'box-shadow: none;' +
'overflow:hidden;' +
'font-family: ' + UI_FONT.PRIMARY + ';' +
'font-size: 14px;' +
'background-color: rgb(' + UI_COLOR.MANGANESE_PURPLE + ');' +
'color:rgb(255, 255, 255);' +
'width: ' + thisObject.container.frame.width + 'px;' +
'height: ' + thisObject.container.frame.height + 'px'
textArea.style.display = 'block'
textArea.focus()
EDITOR_ON_FOCUS = true
}
function getContainer(point) {
let container
if (thisObject.visible === true) {
if (point.x === VERY_LARGE_NUMBER) {
/* The the mouse leaves the canvas, and event of mouse over with a ridiculous coordinate is triggered so that
anyone can react. In our case, the code editor has a text area that is not part of the canvas, so the event is
triggered. We compensate recognizing this coordinate and returning our container. */
return thisObject.container
}
if (thisObject.container.frame.isThisPointHere(point, true, false) === true) {
return thisObject.container
} else {
return undefined
}
}
}
function physics() {
if (Math.abs(thisObject.currentRadius - thisObject.targetRadius) >= 0.5) {
if (thisObject.currentRadius < thisObject.targetRadius) {
thisObject.currentRadius = thisObject.currentRadius + 0.5
} else {
thisObject.currentRadius = thisObject.currentRadius - 0.5
}
}
thisObject.container.frame.position.x = 0
thisObject.container.frame.position.y = 0
thisObject.container.frame.width = thisObject.container.frame.radius * 1 * SIZE_FACTOR
thisObject.container.frame.height = thisObject.container.frame.radius * 1 * SIZE_FACTOR
let textAreaPosition = {
x: 0 - thisObject.container.frame.width / 2,
y: 0 - thisObject.container.frame.height * 3 / 7 + CURRENT_TOP_MARGIN
}
textAreaPosition = thisObject.container.frame.frameThisPoint(textAreaPosition)
if (thisObject.visible === true) {
let checkPosition = {
x: textAreaPosition.x,
y: textAreaPosition.y - CURRENT_TOP_MARGIN
}
if (thisObject.isVisibleFunction(checkPosition) === false) {
deactivate()
}
}
if (thisObject.visible === true) {
let textAreaDiv = document.getElementById('textAreaDiv')
textAreaDiv.style = 'position:fixed; top:' + textAreaPosition.y + 'px; left:' + textAreaPosition.x + 'px; z-index:1; '
}
}
function drawBackground() {
if (thisObject.visible === true) {
let position = {
x: 0,
y: 0
}
position = thisObject.container.frame.frameThisPoint(position)
let radius = thisObject.container.frame.radius * SIZE_FACTOR
if (radius > 0.5) {
browserCanvasContext.beginPath()
browserCanvasContext.arc(position.x, position.y, radius + 3, 0, Math.PI * 2, true)
browserCanvasContext.closePath()
browserCanvasContext.fillStyle = 'rgba(' + UI_COLOR.WHITE + ', ' + 1 + ')'
browserCanvasContext.fill()
browserCanvasContext.beginPath()
browserCanvasContext.arc(position.x, position.y, radius , 0, Math.PI * 2, true)
browserCanvasContext.closePath()
browserCanvasContext.fillStyle = 'rgba(' + UI_COLOR.MANGANESE_PURPLE + ', ' + 1 + ')'
browserCanvasContext.fill()
}
}
}
function drawForeground() {
let iconPosition = {
x: 0,
y: thisObject.currentRadius * 1.5
}
iconPosition = thisObject.container.frame.frameThisPoint(iconPosition)
/* Menu Item */
if (thisObject.canDrawIcon === true && thisObject.currentRadius > 1) {
browserCanvasContext.drawImage(thisObject.icon, iconPosition.x - thisObject.currentRadius, iconPosition.y - thisObject.currentRadius, thisObject.currentRadius * 2, thisObject.currentRadius * 2)
/* Menu Label */
let label = 'Code looks good!'
let labelPoint
let fontSize = 10
browserCanvasContext.font = fontSize + 'px ' + UI_FONT.PRIMARY
if (thisObject.currentRadius >= thisObject.targetRadius) {
labelPoint = {
x: iconPosition.x + thisObject.currentRadius + 10,
y: iconPosition.y + fontSize * FONT_ASPECT_RATIO
}
browserCanvasContext.font = fontSize + 'px ' + UI_FONT.PRIMARY
browserCanvasContext.fillStyle = 'rgba(' + UI_COLOR.WHITE + ', 1)'
browserCanvasContext.fillText(label, labelPoint.x, labelPoint.y)
}
}
UI.projects.foundations.spaces.codeEditorSpace.openSpaceArea(action.node)
}
}
@@ -16,7 +16,6 @@ function newUiObject() {
payload: undefined,
configEditor: undefined,
conditionEditor: undefined,
formulaEditor: undefined,
listSelector: undefined,
uiObjectTitle: undefined,
icon: undefined,
@@ -202,10 +201,6 @@ function newUiObject() {
thisObject.conditionEditor = undefined
}
if (thisObject.formulaEditor !== undefined) {
thisObject.formulaEditor.finalize()
thisObject.formulaEditor = undefined
}
if (thisObject.listSelector !== undefined) {
thisObject.listSelector.finalize()
@@ -303,11 +298,6 @@ function newUiObject() {
if (container !== undefined) { return container }
}
if (thisObject.formulaEditor !== undefined) {
container = thisObject.formulaEditor.getContainer(point)
if (container !== undefined) { return container }
}
if (thisObject.listSelector !== undefined) {
container = thisObject.listSelector.getContainer(point)
if (container !== undefined) { return container }
@@ -326,11 +316,6 @@ function newUiObject() {
getitle = false
}
}
if (thisObject.formulaEditor !== undefined) {
if (thisObject.formulaEditor.visible === true) {
getitle = false
}
}
if (thisObject.listSelector !== undefined) {
if (thisObject.listSelector.visible === true) {
getitle = false
@@ -380,9 +365,6 @@ function newUiObject() {
thisObject.conditionEditor.physics()
}
if (thisObject.formulaEditor !== undefined) {
thisObject.formulaEditor.physics()
}
if (thisObject.listSelector !== undefined) {
thisObject.listSelector.physics()
@@ -1354,9 +1336,7 @@ function newUiObject() {
if (thisObject.conditionEditor !== undefined) {
thisObject.conditionEditor.deactivate()
}
if (thisObject.formulaEditor !== undefined) {
thisObject.formulaEditor.deactivate()
}
if (thisObject.listSelector !== undefined) {
thisObject.listSelector.deactivate()
}
@@ -1383,9 +1363,6 @@ function newUiObject() {
if (thisObject.conditionEditor !== undefined) {
thisObject.conditionEditor.deactivate()
}
if (thisObject.formulaEditor !== undefined) {
thisObject.formulaEditor.deactivate()
}
if (thisObject.listSelector !== undefined) {
thisObject.listSelector.deactivate()
}
@@ -1461,18 +1438,12 @@ function newUiObject() {
drawChainLine()
if (thisObject.configEditor !== undefined) {
thisObject.configEditor.drawBackground()
thisObject.configEditor.drawForeground()
}
if (thisObject.conditionEditor !== undefined) {
thisObject.conditionEditor.drawBackground()
thisObject.conditionEditor.drawForeground()
}
if (thisObject.formulaEditor !== undefined) {
thisObject.formulaEditor.drawBackground()
thisObject.formulaEditor.drawForeground()
}
if (thisObject.listSelector !== undefined) {
thisObject.listSelector.drawBackground()
thisObject.listSelector.drawForeground()
@@ -1494,12 +1465,7 @@ function newUiObject() {
drawTitle = false
}
}
if (thisObject.formulaEditor !== undefined) {
if (thisObject.formulaEditor.visible === true) {
drawMenu = false
drawTitle = false
}
}
if (thisObject.listSelector !== undefined) {
if (thisObject.listSelector.visible === true) {
drawMenu = false
@@ -1678,9 +1644,6 @@ function newUiObject() {
if (thisObject.conditionEditor.visible === true) { return true }
}
if (thisObject.formulaEditor !== undefined) {
if (thisObject.formulaEditor.visible === true) { return true }
}
if (thisObject.listSelector !== undefined) {
if (thisObject.listSelector.visible === true) { return true }
}
@@ -484,8 +484,6 @@ function newUiObjectConstructor() {
}
if (schemaDocument.editors.formula === true) {
uiObject.formulaEditor = newFormulaEditor()
uiObject.formulaEditor.isVisibleFunction = uiObject.isVisibleFunction
uiObject.formulaEditor.container.connectToParent(uiObject.container, false, false, true, true, false, false, false, false)
uiObject.formulaEditor.initialize()
}
if (schemaDocument.editors.list === true) {