From 3cd4ad6a20f8187c7cc2d7e8403689c4f2550463 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Thu, 20 Oct 2022 16:23:12 +0200 Subject: [PATCH] Object properties for autocompletion! --- .../LogarithmPlotter/PickLocationOverlay.qml | 8 +- .../Setting/AutocompletionCategory.qml | 9 +- .../Setting/ExpressionEditor.qml | 86 +++++++++++++++---- .../ad5001/LogarithmPlotter/js/parameters.js | 20 +++++ .../LogarithmPlotter/js/parsing/tokenizer.js | 4 +- 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/PickLocationOverlay.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/PickLocationOverlay.qml index 7e59fd5..5460160 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/PickLocationOverlay.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/PickLocationOverlay.qml @@ -214,9 +214,9 @@ Item { Parses a given \c value as an expression or a number depending on the type of \c propertyName of all \c objType. */ function parseValue(value, objType, propertyName) { - return { - 'Expression': () => new MathLib.Expression(value), - 'number': () => parseFloat(value) - }[Objects.types[objType].properties()[propertyName]]() + if(Objects.types[objType].properties()[propertyName] == 'number') + return parseFloat(value) + else + return new MathLib.Expression(value) } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/AutocompletionCategory.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/AutocompletionCategory.qml index b0d2df9..7e9f683 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/AutocompletionCategory.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/AutocompletionCategory.qml @@ -63,10 +63,17 @@ ListView { Text to autocomplete. */ property string baseText: "" + + /*! + \qmlproperty bool AutocompletionCategory::visbilityCondition + Condition to be met for the category to be visible. + */ + property bool visbilityCondition: true + width: parent.width visible: model.length > 0 implicitHeight: contentItem.childrenRect.height - model: parent.visible ? categoryItems.filter((item) => item.includes(baseText)).map(autocompleteGenerator) : [] + model: visbilityCondition ? categoryItems.filter((item) => item.includes(baseText)).map(autocompleteGenerator) : [] header: Column { width: listFiltered.width diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml index 578ac20..87c3885 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml @@ -200,7 +200,8 @@ Item { Keys.onPressed: function(event) { // Autocomplete popup events - //console.log("Pressed key:", event.key, Qt.Key_Return, Qt.Key_Enter, event.text) + //console.log(acPopupContent.currentToken.dot, acPopupContent.previousToken.dot, "@", acPopupContent.currentToken.identifier, acPopupContent.previousToken.identifier, acPopupContent.previousToken2.identifier, objectPropertiesList.objectName, JSON.stringify(objectPropertiesList.baseText), objectPropertiesList.model.length, JSON.stringify(objectPropertiesList.categoryItems)) + //console.log("Pressed key:", event.key, Qt.Key_Return, Qt.Key_Enter, event.text, acPopupContent.itemCount) if((event.key == Qt.Key_Enter || event.key == Qt.Key_Return) && acPopupContent.itemCount > 0) { acPopupContent.autocomplete() event.accepted = true @@ -251,21 +252,40 @@ Item { Parsing.TokenType.FUNCTION, Parsing.TokenType.CONSTANT ] - property var currentToken: getTokenAt(editor.tokens, editor.cursorPosition) - visible: currentToken != null && identifierTokenTypes.includes(currentToken.type) + property var currentToken: generateTokenInformation(getTokenAt(editor.tokens, editor.cursorPosition)) + property var previousToken: generateTokenInformation(getPreviousToken(currentToken.token)) + property var previousToken2: generateTokenInformation(getPreviousToken(previousToken.token)) + property var previousToken3: generateTokenInformation(getPreviousToken(previousToken2.token)) + visible: currentToken.exists // Focus handling. readonly property var lists: [objectPropertiesList, variablesList, constantsList, functionsList, executableObjectsList, objectsList] - readonly property int itemCount: objectPropertiesList.model.length + variablesList.model.length, constantsList.model.length + functionsList.model.length + executableObjectsList.model.length + objectsList.model.length + readonly property int itemCount: objectPropertiesList.model.length + variablesList.model.length + constantsList.model.length + functionsList.model.length + executableObjectsList.model.length + objectsList.model.length property int itemSelected: 0 /*! - \qmlmethod var ExpressionEditor::autocompleteAt(int idx) + \qmlmethod var ExpressionEditor::generateTokenInformation(var token) + Generates basic information about the given token (existence and type) used in autocompletion). + */ + function generateTokenInformation(token) { + let exists = token != null + return { + 'token': token, + 'exists': exists, + 'value': exists ? token.value : null, + 'type': exists ? token.type : null, + 'startPosition': exists ? token.startPosition : 0, + 'dot': exists ? (token.type == Parsing.TokenType.PUNCT && token.value == ".") : false, + 'identifier': exists ? identifierTokenTypes.includes(token.type) : false + } + } + /*! + \qmlmethod void ExpressionEditor::autocompleteInfoAt(int idx) Returns the autocompletion text information at a given position. The information contains key 'text' (description text), 'autocomplete' (text to insert) and 'cursorFinalOffset' (amount to add to the cursor's position after the end of the autocomplete) */ - function autocompleteAt(idx) { + function autocompleteInfoAt(idx) { if(idx >= itemCount) return "" let startIndex = 0 for(let list of lists) { @@ -274,37 +294,67 @@ Item { startIndex += list.model.length } } + /*! - \qmlmethod var ExpressionEditor::autocomplete() + \qmlmethod void ExpressionEditor::autocomplete() Autocompletes with the current selected word. */ function autocomplete() { - let autotext = autocompleteAt(itemSelected) + let autotext = autocompleteInfoAt(itemSelected) let startPos = currentToken.startPosition + console.log("Replacing", currentToken.value, "at", startPos, "with", autotext.autocomplete) editor.remove(startPos, startPos+currentToken.value.length) editor.insert(startPos, autotext.autocomplete) editor.cursorPosition = startPos+autotext.autocomplete.length+autotext.cursorFinalOffset } + /*! + \qmlmethod var ExpressionEditor::getPreviousToken(var token) + Returns the token before this one. + */ + function getPreviousToken(token) { + let newToken = getTokenAt(editor.tokens, token.startPosition) + if(newToken != null && newToken.type == Parsing.TokenType.WHITESPACE) + return getPreviousToken(newToken) + return newToken + } + AutocompletionCategory { id: objectPropertiesList category: qsTr("Object Properties") + visbilityCondition: isEnteringProperty itemStartIndex: 0 itemSelected: parent.itemSelected - categoryItems: [] - property var objectName: null - autocompleteGenerator: (item) => {return { - 'text': item, 'annotation': Objects.currentObjectsByName[objectName].constructor.properties()[item], - 'autocomplete': item + " ", 'cursorFinalOffset': 0 - }} - baseText: parent.visible ? parent.currentToken.value : "" + property bool isEnteringProperty: ( + // Current token is dot. + (parent.currentToken.dot && parent.previousToken.identifier && !parent.previousToken2.dot) || + // Current token is property identifier + (parent.currentToken.identifier && parent.previousToken.dot && parent.previousToken2.identifier && !parent.previousToken3.dot)) + property string objectName: isEnteringProperty ? + (parent.currentToken.dot ? parent.previousToken.value : parent.previousToken2.value) + : "" + property var objectProperties: isEnteringProperty ? + Objects.currentObjectsByName[objectName].constructor.properties() : + {} + categoryItems: Object.keys(objectProperties) + autocompleteGenerator: (item) => { + let propType = objectProperties[item] + return { + 'text': item, 'annotation': propType == null ? '' : propType.toString(), + 'autocomplete': parent.currentToken.dot ? `.${item} ` : `${item} `, + 'cursorFinalOffset': 0 + } + + } + baseText: parent.visible && !parent.currentToken.dot ? parent.currentToken.value : "" } AutocompletionCategory { id: variablesList category: qsTr("Variables") + visbilityCondition: parent.currentToken.identifier && !parent.previousToken.dot itemStartIndex: objectPropertiesList.model.length itemSelected: parent.itemSelected categoryItems: control.variables @@ -319,6 +369,7 @@ Item { id: constantsList category: qsTr("Constants") + visbilityCondition: parent.currentToken.identifier && !parent.previousToken.dot itemStartIndex: variablesList.itemStartIndex + variablesList.model.length itemSelected: parent.itemSelected categoryItems: Parsing.CONSTANTS_LIST @@ -333,6 +384,7 @@ Item { id: functionsList category: qsTr("Functions") + visbilityCondition: parent.currentToken.identifier && !parent.previousToken.dot itemStartIndex: constantsList.itemStartIndex + constantsList.model.length itemSelected: parent.itemSelected categoryItems: Parsing.FUNCTIONS_LIST @@ -347,11 +399,12 @@ Item { id: executableObjectsList category: qsTr("Executable Objects") + visbilityCondition: parent.currentToken.identifier && !parent.previousToken.dot itemStartIndex: functionsList.itemStartIndex + functionsList.model.length itemSelected: parent.itemSelected categoryItems: Objects.getObjectsName("ExecutableObject").filter(obj => obj != self) autocompleteGenerator: (item) => {return { - 'text': item, 'annotation': `${Objects.currentObjectsByName[item].constructor.displayType()}`, + 'text': item, 'annotation': Objects.currentObjectsByName[item] == null ? '' : Objects.currentObjectsByName[item].constructor.displayType(), 'autocomplete': item+'()', 'cursorFinalOffset': -1 }} baseText: parent.visible ? parent.currentToken.value : "" @@ -361,6 +414,7 @@ Item { id: objectsList category: qsTr("Objects") + visbilityCondition: parent.currentToken.identifier && !parent.previousToken.dot itemStartIndex: executableObjectsList.itemStartIndex + executableObjectsList.model.length itemSelected: parent.itemSelected categoryItems: Object.keys(Objects.currentObjectsByName).filter(obj => obj != self) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parameters.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parameters.js index 1733a90..c8d34ca 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parameters.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parameters.js @@ -21,6 +21,10 @@ class Expression { this.type = 'Expression' this.variables = variables } + + toString() { + return this.variables.length == 0 ? 'Number' : `Expression(${this.variables.join(', ')})` + } } class Enum { @@ -29,6 +33,10 @@ class Enum { this.values = values this.translatedValues = values.map(x => qsTr(x)) } + + toString() { + return this.type + } } class ObjectType { @@ -36,6 +44,10 @@ class ObjectType { this.type = 'ObjectType' this.objType = objType } + + toString() { + return this.objType + } } class List { @@ -47,6 +59,10 @@ class List { this.label = label this.forbidAdding = forbidAdding } + + toString() { + return this.objType + } } class Dictionary { @@ -61,6 +77,10 @@ class Dictionary { this.postKeyLabel = postKeyLabel this.forbidAdding = forbidAdding } + + toString() { + return 'Dictionary' + } } // Common parameters for Enums diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js index 67a2a41..63358c0 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js @@ -24,7 +24,7 @@ const WHITESPACES = " \t\n\r" const STRING_LIMITORS = '"\'`'; const OPERATORS = "+-*/^%?:=!><"; const PUNCTUTATION = "()[]{},."; -const NUMBER_CHARS = "0123456789." +const NUMBER_CHARS = "0123456789" const IDENTIFIER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_₀₁₂₃₄₅₆₇₈₉αβγδεζηθκλμξρςστφχψωₐₑₒₓₔₕₖₗₘₙₚₛₜ" var TokenType = { @@ -92,7 +92,7 @@ class ExpressionTokenizer { readNumber() { let included = ""; let hasDot = false; - while(!this.input.atEnd() && NUMBER_CHARS.includes(this.input.peek())) { + while(!this.input.atEnd() && (NUMBER_CHARS.includes(this.input.peek()) || this.input.peek() == '.')) { if(this.input.peek() == ".") { if(hasDot) this.input.raise("Unexpected '.'. Expected digit") hasDot = true;