[apps/code] Autocomplete at the end of tokens

Not at the end of "words" separated by spaces.
This commit is contained in:
Léa Saviot
2020-04-24 10:59:18 +02:00
committed by Émilie Feral
parent 9bf361d5a5
commit da2730dd64
6 changed files with 107 additions and 33 deletions

View File

@@ -124,11 +124,21 @@ bool EditorController::textAreaDidReceiveEvent(TextArea * textArea, Ion::Events:
VariableBoxController * EditorController::variableBoxForInputEventHandler(InputEventHandler * textInput) {
VariableBoxController * varBox = App::app()->variableBoxController();
/* If the editor should be autocompleting, the variable box has already been
* loaded. We check shouldAutocomplete and not isAutocompleting, because the
* autocompletion result might be empty. */
if (!m_editorView.shouldAutocomplete()) {
/* If the editor should be autocompleting an identifier, the variable box has
* already been loaded. We check shouldAutocomplete and not isAutocompleting,
* because the autocompletion result might be empty. */
const char * beginningOfAutocompletion = nullptr;
const char * cursor = nullptr;
PythonTextArea::AutocompletionType autocompType = m_editorView.autocompletionType(&beginningOfAutocompletion, &cursor);
if (autocompType == PythonTextArea::AutocompletionType::NoIdentifier) {
varBox->loadFunctionsAndVariables(m_scriptIndex, nullptr, 0);
} else if (autocompType == PythonTextArea::AutocompletionType::MiddleOfIdentifier) {
varBox->empty();
} else {
assert(autocompType == PythonTextArea::AutocompletionType::EndOfIdentifier);
assert(beginningOfAutocompletion != nullptr && cursor != nullptr);
assert(cursor > beginningOfAutocompletion);
varBox->loadFunctionsAndVariables(m_scriptIndex, beginningOfAutocompletion, cursor - beginningOfAutocompletion);
}
return varBox;
}

View File

@@ -9,7 +9,7 @@ namespace Code {
class EditorView : public Responder, public View, public ScrollViewDelegate {
public:
EditorView(Responder * parentResponder, App * pythonDelegate);
bool shouldAutocomplete() const { return m_textArea.shouldAutocomplete(); }
PythonTextArea::AutocompletionType autocompletionType(const char ** autocompletionBeginning, const char ** autocompletionEnd) const { return m_textArea.autocompletionType(nullptr, autocompletionBeginning, autocompletionEnd); }
bool isAutocompleting() const;
void resetSelection();
void setTextAreaDelegates(InputEventHandlerDelegate * inputEventHandlerDelegate, TextAreaDelegate * delegate) {
@@ -19,6 +19,9 @@ public:
void setText(char * textBuffer, size_t textBufferSize) {
m_textArea.setText(textBuffer, textBufferSize);
}
const char * cursorLocation() {
return m_textArea.cursorLocation();
}
bool setCursorLocation(const char * location) {
return m_textArea.setCursorLocation(location);
}

View File

@@ -53,14 +53,64 @@ static inline size_t TokenLength(mp_lexer_t * lex, const char * tokenPosition) {
}
return lex->column - lex->tok_column;
}
bool PythonTextArea::shouldAutocomplete(const char * autocompletionLocation) const {
if (isAutocompleting()) {
return true;
}
PythonTextArea::AutocompletionType PythonTextArea::autocompletionType(const char * autocompletionLocation, const char ** autocompletionLocationBeginning, const char ** autocompletionLocationEnd) const {
const char * location = autocompletionLocation != nullptr ? autocompletionLocation : cursorLocation();
CodePoint prevCodePoint = UTF8Helper::PreviousCodePoint(m_contentView.editedText(), location);
return !UTF8Helper::CodePointIsEndOfWord(prevCodePoint)
&& UTF8Helper::CodePointIsEndOfWord(UTF8Helper::CodePointAtLocation(location));
const char * beginningOfToken = nullptr;
// We want to autocomplete only at the end of an identifier or a keyword
AutocompletionType autocompleteType = AutocompletionType::NoIdentifier;
nlr_buf_t nlr;
if (nlr_push(&nlr) == 0) {
const char * firstNonSpace = UTF8Helper::BeginningOfWord(m_contentView.editedText(), location);
mp_lexer_t * lex = mp_lexer_new_from_str_len(0, firstNonSpace, UTF8Helper::EndOfWord(location) - firstNonSpace, 0);
const char * tokenStart;
const char * tokenEnd;
_mp_token_kind_t currentTokenKind = lex->tok_kind;
while (currentTokenKind != MP_TOKEN_NEWLINE && currentTokenKind != MP_TOKEN_END) {
tokenStart = firstNonSpace + lex->tok_column - 1;
tokenEnd = tokenStart + TokenLength(lex, tokenStart);
if (location < tokenStart) {
// The location for autocompletion is not in an identifier
break;
}
if (currentTokenKind == MP_TOKEN_NAME
|| (currentTokenKind >= MP_TOKEN_KW_FALSE
&& currentTokenKind <= MP_TOKEN_KW_YIELD))
{
if (location < tokenEnd) {
// The location for autocompletion is in the middle of an identifier
autocompleteType = AutocompletionType::MiddleOfIdentifier;
break;
}
if (location == tokenEnd) {
// The location for autocompletion is at the end of an identifier
beginningOfToken = tokenStart;
autocompleteType = AutocompletionType::EndOfIdentifier;
break;
}
}
if (location < tokenStart) {
// The location for autocompletion is not in an identifier
break;
}
mp_lexer_to_next(lex);
currentTokenKind = lex->tok_kind;
}
mp_lexer_free(lex);
nlr_pop();
}
if (autocompletionLocationBeginning != nullptr) {
*autocompletionLocationBeginning = beginningOfToken;
}
if (autocompletionLocationEnd != nullptr) {
*autocompletionLocationEnd = location;
}
assert(!isAutocompleting() || autocompleteType == AutocompletionType::EndOfIdentifier);
return autocompleteType;
}
const char * PythonTextArea::ContentView::textToAutocomplete() const {
@@ -281,18 +331,18 @@ void PythonTextArea::removeAutocompletion() {
void PythonTextArea::addAutocompletion() {
assert(!m_contentView.isAutocompleting());
const char * autocompletionTokenBeginning = nullptr;
const char * autocompletionLocation = const_cast<char *>(cursorLocation());
const char * textToInsert = nullptr;
int textToInsertLength = 0;
if (shouldAutocomplete(autocompletionLocation)) {
/* The previous code point is neither the beginning of the text, nor a
* space, nor a \n, and the next code point is the end of the word.
if (autocompletionType(autocompletionLocation, &autocompletionTokenBeginning) == AutocompletionType::EndOfIdentifier) {
/* The cursor is at the end of an identifier.
* Compute the text to insert:
* Look first in the current script variables and functions, then in the
* builtins, then in the imported modules/scripts. */
VariableBoxController * varBox = m_contentView.pythonDelegate()->variableBoxController();
const char * beginningOfWord = m_contentView.textToAutocomplete();
textToInsert = varBox->autocompletionForText(m_contentView.pythonDelegate()->menuController()->editedScriptIndex(), beginningOfWord, &textToInsertLength);
const int scriptIndex = m_contentView.pythonDelegate()->menuController()->editedScriptIndex();
textToInsert = varBox->autocompletionForText(scriptIndex, autocompletionTokenBeginning, autocompletionLocation - autocompletionTokenBeginning, &textToInsertLength);
}
// Try to insert the text (this might fail if the buffer is full)

View File

@@ -9,6 +9,11 @@ class App;
class PythonTextArea : public TextArea {
public:
enum class AutocompletionType : uint8_t {
EndOfIdentifier,
MiddleOfIdentifier,
NoIdentifier
};
PythonTextArea(Responder * parentResponder, App * pythonDelegate, const KDFont * font) :
TextArea(parentResponder, &m_contentView, font),
m_contentView(pythonDelegate, font)
@@ -18,10 +23,14 @@ public:
void unloadSyntaxHighlighter() { m_contentView.unloadSyntaxHighlighter(); }
bool handleEvent(Ion::Events::Event event) override;
bool handleEventWithText(const char * text, bool indentation = false, bool forceCursorRightOfText = false) override;
/* shouldAutocomplete returns true if there is currently autocompletion, or if
* there should be autocompletion but there is not because there is no word to
* autocomplete. */
bool shouldAutocomplete(const char * autocompletionLocation = nullptr) const;
/* autocompletionType returns:
* - EndOfIdentifier if there is currently autocompletion, or if the wursor is
* at the end of an identifier,
* - MiddleOfIdentifier is the cursor is in the middle of an identifier,
* - No identifier otherwise.
* The autocompletionLocation can be provided with autocompletionLocation, or
* retreived with autocompletionLocationBeginning and autocompletionLocationEnd. */
AutocompletionType autocompletionType(const char * autocompletionLocation = nullptr, const char ** autocompletionLocationBeginning = nullptr, const char ** autocompletionLocationEnd = nullptr) const;
bool isAutocompleting() const { return m_contentView.isAutocompleting(); }
protected:
class ContentView : public TextArea::ContentView {

View File

@@ -190,16 +190,11 @@ void VariableBoxController::loadFunctionsAndVariables(int scriptIndex, const cha
}
}
const char * VariableBoxController::autocompletionForText(int scriptIndex, const char * textToAutocomplete, int * textToInsertLength) {
/* The text to autocomplete will most probably not be null-terminated. We thus
* say that the end of the text to autocomplete is the end if the current word,
* determined by the next space char or the next null char.*/
const char * endOfText = UTF8Helper::EndOfWord(textToAutocomplete);
const int textLength = endOfText - textToAutocomplete;
assert(textLength >= 1);
const char * VariableBoxController::autocompletionForText(int scriptIndex, const char * textToAutocomplete, int textToAutocompleteLength, int * textToInsertLength) {
assert(textToAutocompleteLength >= 1);
// First load variables and functions that complete the textToAutocomplete
loadFunctionsAndVariables(scriptIndex, textToAutocomplete, textLength);
loadFunctionsAndVariables(scriptIndex, textToAutocomplete, textToAutocompleteLength);
if (numberOfRows() == 0) {
return nullptr;
}
@@ -212,10 +207,16 @@ const char * VariableBoxController::autocompletionForText(int scriptIndex, const
currentNameLength = strlen(currentName);
}
// Assert the text we return does indeed autocomplete the text to autocomplete
assert(currentNameLength != textLength && strncmp(textToAutocomplete, currentName, textLength) == 0);
assert(currentNameLength != textToAutocompleteLength && strncmp(textToAutocomplete, currentName, textToAutocompleteLength) == 0);
// Return the text without the beginning that matches the text to autocomplete
*textToInsertLength = currentNameLength - textLength;
return currentName + textLength;
*textToInsertLength = currentNameLength - textToAutocompleteLength;
return currentName + textToAutocompleteLength;
}
void VariableBoxController::empty() {
m_builtinNodesCount = 0;
m_currentScriptNodesCount = 0;
m_importedNodesCount = 0;
}
// PRIVATE METHODS

View File

@@ -35,7 +35,8 @@ public:
/* VariableBoxController */
void loadFunctionsAndVariables(int scriptIndex, const char * textToAutocomplete, int textToAutocompleteLength);
const char * autocompletionForText(int scriptIndex, const char * textToAutocomplete, int * textToInsertLength);
const char * autocompletionForText(int scriptIndex, const char * textToAutocomplete, int textToAutocompleteLength, int * textToInsertLength);
void empty();
private:
//TODO LEA use size_t