diff --git a/apps/code/editor_controller.cpp b/apps/code/editor_controller.cpp index 1325f21eb..49a9fee8b 100644 --- a/apps/code/editor_controller.cpp +++ b/apps/code/editor_controller.cpp @@ -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; } diff --git a/apps/code/editor_view.h b/apps/code/editor_view.h index 991aaf062..ab3038c94 100644 --- a/apps/code/editor_view.h +++ b/apps/code/editor_view.h @@ -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); } diff --git a/apps/code/python_text_area.cpp b/apps/code/python_text_area.cpp index b4be26104..103f66f9e 100644 --- a/apps/code/python_text_area.cpp +++ b/apps/code/python_text_area.cpp @@ -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(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) diff --git a/apps/code/python_text_area.h b/apps/code/python_text_area.h index 1bbcd2531..28a69a5e0 100644 --- a/apps/code/python_text_area.h +++ b/apps/code/python_text_area.h @@ -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 { diff --git a/apps/code/variable_box_controller.cpp b/apps/code/variable_box_controller.cpp index 652687968..faea142d1 100644 --- a/apps/code/variable_box_controller.cpp +++ b/apps/code/variable_box_controller.cpp @@ -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 diff --git a/apps/code/variable_box_controller.h b/apps/code/variable_box_controller.h index 6efe0228d..4b69510af 100644 --- a/apps/code/variable_box_controller.h +++ b/apps/code/variable_box_controller.h @@ -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