First crack at autocompletion.

This commit is contained in:
Ad5001 2022-10-19 23:44:04 +02:00
parent 16efe31b5f
commit 5da8dcefe5
Signed by: Ad5001
GPG key ID: 7251B1AF90B960F9
6 changed files with 193 additions and 22 deletions

View file

@ -58,6 +58,9 @@ D.Dialog {
width: 350 width: 350
height: 400 height: 400
// Disable closing on return/enter, causing issues with autocomplete.
onActionChosen: if(action.key == Qt.Key_Enter || action.key == Qt.Key_Return) action.accepted = false
Label { Label {
id: dlgTitle id: dlgTitle
anchors.left: parent.left anchors.left: parent.left

View file

@ -49,7 +49,7 @@ ScrollView {
id: objectsListView id: objectsListView
model: Object.keys(Objects.types) model: Object.keys(Objects.types)
//width: implicitWidth //objectListList.width - (implicitHeight > objectListList.parent.height ? 20 : 0) //width: implicitWidth //objectListList.width - (implicitHeight > objectListList.parent.height ? 20 : 0)
implicitHeight: contentItem.childrenRect.height + footer.height + 10 implicitHeight: contentItem.childrenRect.height + footerItem.height + 10
delegate: ListView { delegate: ListView {
id: objTypeList id: objTypeList

View file

@ -19,7 +19,7 @@
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Dialogs 1.3 as D import QtQuick.Dialogs 1.3 as D
import eu.ad5001.LogarithmPlotter.Popup 1.0 as Popup import eu.ad5001.LogarithmPlotter.Popup 1.0 as P
import "../js/mathlib.js" as MathLib import "../js/mathlib.js" as MathLib
import "../js/utils.js" as Utils import "../js/utils.js" as Utils
import "../js/parsing/parsing.js" as Parsing import "../js/parsing/parsing.js" as Parsing
@ -151,6 +151,8 @@ Item {
focus: true focus: true
selectByMouse: true selectByMouse: true
property var tokens: parent.tokens(text)
Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses. Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses.
onEditingFinished: { onEditingFinished: {
@ -165,7 +167,38 @@ Item {
} }
} }
//onTextEdited: acPopupContent.itemSelected = 0
onActiveFocusChanged: {
if(activeFocus)
autocompletePopup.open()
else
autocompletePopup.close()
}
Keys.onUpPressed: function(event) {
acPopupContent.itemSelected = Math.max(0, acPopupContent.itemSelected-1)
event.accepted = true
}
Keys.onDownPressed: function(event) {
acPopupContent.itemSelected = Math.max(0,Math.min(acPopupContent.itemCount-1, acPopupContent.itemSelected+1))
event.accepted = true
}
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
// Autocomplete popup events
//console.log("Pressed key:", event.key, Qt.Key_Return, Qt.Key_Enter, event.text)
if((event.key == Qt.Key_Enter || event.key == Qt.Key_Return) && acPopupContent.itemCount > 0) {
acPopupContent.autocomplete()
event.accepted = true
} else
acPopupContent.itemSelected = 0
/*if(event.key == Qt.Key_Left) { // TODO: Don't reset the position when the key moved is still on the same word
if(!acPopupContent.identifierTokenTypes.includes())
}*/
if(event.text in openAndCloseMatches) { if(event.text in openAndCloseMatches) {
let start = selectionStart let start = selectionStart
insert(selectionStart, event.text) insert(selectionStart, event.text)
@ -181,11 +214,116 @@ Item {
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter
textFormat: Text.StyledText textFormat: Text.StyledText
text: colorize(editor.text) text: colorize(parent.tokens)
color: sysPalette.windowText color: sysPalette.windowText
//font.pixelSize: parent.font.pixelSize //font.pixelSize: parent.font.pixelSize
//opacity: editor.activeFocus ? 0 : 1 //opacity: editor.activeFocus ? 0 : 1
} }
Popup {
id: autocompletePopup
x: 0
y: parent.height
closePolicy: Popup.NoAutoClose
width: editor.width
height: acPopupContent.height
padding: 0
Column {
id: acPopupContent
width: parent.width
readonly property var identifierTokenTypes: [
Parsing.TokenType.VARIABLE,
Parsing.TokenType.FUNCTION,
Parsing.TokenType.CONSTANT
]
property var currentToken: getTokenAt(editor.tokens, editor.cursorPosition)
visible: currentToken != null && identifierTokenTypes.includes(currentToken.type)
// Focus handling.
readonly property var lists: [functionsList]
readonly property int itemCount: functionsList.model.length
property int itemSelected: 0
/*!
\qmlmethod var ExpressionEditor::autocompleteAt(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) {
if(idx >= itemCount) return ""
let startIndex = 0
for(let list of lists) {
if(idx < startIndex + list.model.length)
return list.model[idx-startIndex]
startIndex += list.model.length
}
}
/*!
\qmlmethod var ExpressionEditor::autocomplete()
Autocompletes with the current selected word.
*/
function autocomplete() {
let autotext = autocompleteAt(itemSelected)
let startPos = currentToken.startPosition
console.log("Autocompleting",autotext.text,startPos)
editor.remove(startPos, startPos+currentToken.value.length)
editor.insert(startPos, autotext.autocomplete)
editor.cursorPosition = startPos+autotext.autocomplete.length+autotext.cursorFinalOffset
}
ListView {
id: functionsList
//anchors.fill: parent
property int itemStartIndex: 0
width: parent.width
visible: model.length > 0
implicitHeight: contentItem.childrenRect.height + headerItem.height
model: parent.visible ?
Parsing.FUNCTIONS_LIST.filter((name) => name.includes(acPopupContent.currentToken.value))
.map((name) => {return {'text': name, 'autocomplete': name+'()', 'cursorFinalOffset': -1}}) : []
header: Column {
width: functionsList.width
spacing: 2
topPadding: 5
bottomPadding: 5
Text {
leftPadding: 5
text: qsTr("Functions")
}
Rectangle {
height: 1
color: 'black'
width: parent.width
}
}
delegate: Rectangle {
property bool selected: index + functionsList.itemStartIndex == acPopupContent.itemSelected
width: funcText.width
height: funcText.height
color: selected ? sysPalette.highlight : 'transparent'
Text {
id: funcText
topPadding: 2
bottomPadding: 2
leftPadding: 15
text: functionsList.model[index].text
width: functionsList.width
color: parent.selected ? sysPalette.highlightedText : sysPalette.windowText
}
}
}
}
}
} }
Button { Button {
@ -202,7 +340,7 @@ Item {
} }
} }
Popup.InsertCharacter { P.InsertCharacter {
id: insertPopup id: insertPopup
x: Math.round((parent.width - width) / 2) x: Math.round((parent.width - width) / 2)
@ -228,7 +366,6 @@ Item {
// Check if the expression is valid, throws error otherwise. // Check if the expression is valid, throws error otherwise.
if(!expr.allRequirementsFullfilled()) { if(!expr.allRequirementsFullfilled()) {
let undefVars = expr.undefinedVariables() let undefVars = expr.undefinedVariables()
console.log(JSON.stringify(undefVars), undefVars.join(', '))
if(undefVars.length > 1) if(undefVars.length > 1)
throw new Error(qsTranslate('error', 'No object found with names %1.').arg(undefVars.join(', '))) throw new Error(qsTranslate('error', 'No object found with names %1.').arg(undefVars.join(', ')))
else else
@ -245,15 +382,41 @@ Item {
} }
/*! /*!
\qmlmethod var ExpressionEditor::colorize(string expressionText) \qmlmethod var ExpressionEditor::tokens(string expressionText)
Creates an HTML colorized string of the incomplete \c expressionText. Generates a list of tokens from the given.
*/
function tokens(text) {
let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, false)
let tokenList = []
let token
while((token = tokenizer.next()) != null)
tokenList.push(token)
return tokenList
}
/*!
\qmlmethod var ExpressionEditor::getTokenAt(var tokens, int position)
Gets the token at the given position within the text.
Returns null if out of bounds.
*/
function getTokenAt(tokenList, position) {
let currentPosition = 0
for(let token of tokenList)
if(position <= (currentPosition + token.value.length))
return token
else
currentPosition += token.value.length
return null
}
/*!
\qmlmethod var ExpressionEditor::colorize(var tokenList)
Creates an HTML colorized string of the given tokens.
Returns the colorized and escaped expression if possible, null otherwise.. Returns the colorized and escaped expression if possible, null otherwise..
*/ */
function colorize(text) { function colorize(tokenList) {
let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, false)
let parsedText = "" let parsedText = ""
let token for(let token of tokenList) {
while((token = tokenizer.next()) != null) {
switch(token.type) { switch(token.type) {
case Parsing.TokenType.VARIABLE: case Parsing.TokenType.VARIABLE:
parsedText += `<font color="${colorScheme.VARIABLE}">${token.value}</font>` parsedText += `<font color="${colorScheme.VARIABLE}">${token.value}</font>`

View file

@ -26,3 +26,5 @@ var Input = Common.InputExpression
var TokenType = TK.TokenType var TokenType = TK.TokenType
var Token = TK.Token var Token = TK.Token
var Tokenizer = TK.ExpressionTokenizer var Tokenizer = TK.ExpressionTokenizer
var FUNCTIONS_LIST = Reference.FUNCTIONS_LIST

View file

@ -64,6 +64,8 @@ const FUNCTIONS = {
"tan": Math.tan, "tan": Math.tan,
"tanh": Math.tanh, "tanh": Math.tanh,
"trunc": Math.trunc, "trunc": Math.trunc,
"integral": () => 0, // TODO: Implement
"derivative": () => 0,
} }
const FUNCTIONS_LIST = Object.keys(FUNCTIONS); const FUNCTIONS_LIST = Object.keys(FUNCTIONS);
// TODO: Complete // TODO: Complete

View file

@ -41,9 +41,10 @@ var TokenType = {
} }
class Token { class Token {
constructor(type, value) { constructor(type, value, startPosition) {
this.type = type; this.type = type;
this.value = value; this.value = value;
this.startPosition = startPosition
} }
} }
@ -65,7 +66,7 @@ class ExpressionTokenizer {
while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek())) { while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek())) {
included += this.input.next(); included += this.input.next();
} }
return new Token(TokenType.WHITESPACE, included) return new Token(TokenType.WHITESPACE, included, this.input.position-included.length)
} }
readString() { readString() {
@ -80,7 +81,7 @@ class ExpressionTokenizer {
included += this.input.next(); included += this.input.next();
} }
this.input.skip(delimitation) this.input.skip(delimitation)
let token = new Token(TokenType.STRING, included) let token = new Token(TokenType.STRING, included, this.input.position-included.length)
token.limitator = delimitation token.limitator = delimitation
return token return token
} else { } else {
@ -98,7 +99,7 @@ class ExpressionTokenizer {
} }
included += this.input.next(); included += this.input.next();
} }
return new Token(TokenType.NUMBER, included) return new Token(TokenType.NUMBER, included, this.input.position-included.length)
} }
readOperator() { readOperator() {
@ -106,7 +107,7 @@ class ExpressionTokenizer {
while(!this.input.atEnd() && OPERATORS.includes(this.input.peek())) { while(!this.input.atEnd() && OPERATORS.includes(this.input.peek())) {
included += this.input.next(); included += this.input.next();
} }
return new Token(TokenType.OPERATOR, included) return new Token(TokenType.OPERATOR, included, this.input.position-included.length)
} }
readIdentifier() { readIdentifier() {
@ -115,11 +116,11 @@ class ExpressionTokenizer {
identifier += this.input.next(); identifier += this.input.next();
} }
if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) { if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) {
return new Token(TokenType.CONSTANT, identifier.toLowerCase()) return new Token(TokenType.CONSTANT, identifier.toLowerCase(), this.input.position-identifier.length)
} else if(Reference.FUNCTIONS_LIST.includes(identifier.toLowerCase())) { } else if(Reference.FUNCTIONS_LIST.includes(identifier.toLowerCase())) {
return new Token(TokenType.FUNCTION, identifier.toLowerCase()) return new Token(TokenType.FUNCTION, identifier.toLowerCase(), this.input.position-identifier.length)
} else { } else {
return new Token(TokenType.VARIABLE, identifier) return new Token(TokenType.VARIABLE, identifier, this.input.position-identifier.length)
} }
} }
@ -133,12 +134,12 @@ class ExpressionTokenizer {
if(NUMBER_CHARS.includes(c)) return this.readNumber(); if(NUMBER_CHARS.includes(c)) return this.readNumber();
if(IDENTIFIER_CHARS.includes(c.toLowerCase())) return this.readIdentifier(); if(IDENTIFIER_CHARS.includes(c.toLowerCase())) return this.readIdentifier();
if(OPERATORS.includes(c)) return this.readOperator(); if(OPERATORS.includes(c)) return this.readOperator();
if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, c); if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, this.input.next(), this.input.position-1);
if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, this.input.next()); if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, this.input.next(), this.input.position-1);
if(this.errorOnUnknown) if(this.errorOnUnknown)
this.input.throw("Unknown token character " + c) this.input.throw("Unknown token character " + c)
else else
return new Token(TokenType.UNKNOWN, this.input.next()); return new Token(TokenType.UNKNOWN, this.input.next(), this.input.position-1);
} }
peek() { peek() {