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
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 {
id: dlgTitle
anchors.left: parent.left

View file

@ -49,7 +49,7 @@ ScrollView {
id: objectsListView
model: Object.keys(Objects.types)
//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 {
id: objTypeList

View file

@ -19,7 +19,7 @@
import QtQuick.Controls 2.12
import QtQuick 2.12
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/utils.js" as Utils
import "../js/parsing/parsing.js" as Parsing
@ -151,6 +151,8 @@ Item {
focus: true
selectByMouse: true
property var tokens: parent.tokens(text)
Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses.
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) {
// 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) {
let start = selectionStart
insert(selectionStart, event.text)
@ -181,11 +214,116 @@ Item {
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter
textFormat: Text.StyledText
text: colorize(editor.text)
text: colorize(parent.tokens)
color: sysPalette.windowText
//font.pixelSize: parent.font.pixelSize
//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 {
@ -202,7 +340,7 @@ Item {
}
}
Popup.InsertCharacter {
P.InsertCharacter {
id: insertPopup
x: Math.round((parent.width - width) / 2)
@ -228,7 +366,6 @@ Item {
// Check if the expression is valid, throws error otherwise.
if(!expr.allRequirementsFullfilled()) {
let undefVars = expr.undefinedVariables()
console.log(JSON.stringify(undefVars), undefVars.join(', '))
if(undefVars.length > 1)
throw new Error(qsTranslate('error', 'No object found with names %1.').arg(undefVars.join(', ')))
else
@ -245,15 +382,41 @@ Item {
}
/*!
\qmlmethod var ExpressionEditor::colorize(string expressionText)
Creates an HTML colorized string of the incomplete \c expressionText.
\qmlmethod var ExpressionEditor::tokens(string 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..
*/
function colorize(text) {
let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, false)
function colorize(tokenList) {
let parsedText = ""
let token
while((token = tokenizer.next()) != null) {
for(let token of tokenList) {
switch(token.type) {
case Parsing.TokenType.VARIABLE:
parsedText += `<font color="${colorScheme.VARIABLE}">${token.value}</font>`

View file

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

View file

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

View file

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