First crack at autocompletion.
This commit is contained in:
parent
16efe31b5f
commit
5da8dcefe5
6 changed files with 193 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue