#include "python_text_area.h" #include "app.h" #include #include #include #include "../global_preferences.h" extern "C" { #include "py/nlr.h" #include "py/lexer.h" } #include #include namespace Code { constexpr KDColor CommentColor = Palette::CodeComment; constexpr KDColor NumberColor = Palette::CodeNumber; constexpr KDColor KeywordColor = Palette::CodeKeyword; // constexpr KDColor BuiltinColor = KDColor::RGB24(0x0086B3); constexpr KDColor OperatorColor = Palette::CodeOperator; constexpr KDColor StringColor = Palette::CodeString; constexpr KDColor BackgroundColor = Palette::CodeBackground; constexpr KDColor HighlightColor = Palette::CodeBackgroundSelected; constexpr KDColor AutocompleteColor = KDColor::RGB24(0xC6C6C6); // TODO Palette change bool isItalic(mp_token_kind_t tokenKind) { if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { return false; } if (tokenKind == MP_TOKEN_STRING) { return true; } static_assert(MP_TOKEN_ELLIPSIS + 1 == MP_TOKEN_KW_FALSE && MP_TOKEN_KW_FALSE + 1 == MP_TOKEN_KW_NONE && MP_TOKEN_KW_NONE + 1 == MP_TOKEN_KW_TRUE && MP_TOKEN_KW_TRUE + 1 == MP_TOKEN_KW___DEBUG__ && MP_TOKEN_KW___DEBUG__ + 1 == MP_TOKEN_KW_AND && MP_TOKEN_KW_AND + 1 == MP_TOKEN_KW_AS && MP_TOKEN_KW_AS + 1 == MP_TOKEN_KW_ASSERT /* Here there are keywords that depend on MICROPY_PY_ASYNC_AWAIT, we do * not test them */ && MP_TOKEN_KW_BREAK + 1 == MP_TOKEN_KW_CLASS && MP_TOKEN_KW_CLASS + 1 == MP_TOKEN_KW_CONTINUE && MP_TOKEN_KW_CONTINUE + 1 == MP_TOKEN_KW_DEF && MP_TOKEN_KW_DEF + 1 == MP_TOKEN_KW_DEL && MP_TOKEN_KW_DEL + 1 == MP_TOKEN_KW_ELIF && MP_TOKEN_KW_ELIF + 1 == MP_TOKEN_KW_ELSE && MP_TOKEN_KW_ELSE + 1 == MP_TOKEN_KW_EXCEPT && MP_TOKEN_KW_EXCEPT + 1 == MP_TOKEN_KW_FINALLY && MP_TOKEN_KW_FINALLY + 1 == MP_TOKEN_KW_FOR && MP_TOKEN_KW_FOR + 1 == MP_TOKEN_KW_FROM && MP_TOKEN_KW_FROM + 1 == MP_TOKEN_KW_GLOBAL && MP_TOKEN_KW_GLOBAL + 1 == MP_TOKEN_KW_IF && MP_TOKEN_KW_IF + 1 == MP_TOKEN_KW_IMPORT && MP_TOKEN_KW_IMPORT + 1 == MP_TOKEN_KW_IN && MP_TOKEN_KW_IN + 1 == MP_TOKEN_KW_IS && MP_TOKEN_KW_IS + 1 == MP_TOKEN_KW_LAMBDA && MP_TOKEN_KW_LAMBDA + 1 == MP_TOKEN_KW_NONLOCAL && MP_TOKEN_KW_NONLOCAL + 1 == MP_TOKEN_KW_NOT && MP_TOKEN_KW_NOT + 1 == MP_TOKEN_KW_OR && MP_TOKEN_KW_OR + 1 == MP_TOKEN_KW_PASS && MP_TOKEN_KW_PASS + 1 == MP_TOKEN_KW_RAISE && MP_TOKEN_KW_RAISE + 1 == MP_TOKEN_KW_RETURN && MP_TOKEN_KW_RETURN + 1 == MP_TOKEN_KW_TRY && MP_TOKEN_KW_TRY + 1 == MP_TOKEN_KW_WHILE && MP_TOKEN_KW_WHILE + 1 == MP_TOKEN_KW_WITH && MP_TOKEN_KW_WITH + 1 == MP_TOKEN_KW_YIELD && MP_TOKEN_KW_YIELD + 1 == MP_TOKEN_OP_ASSIGN && MP_TOKEN_OP_ASSIGN + 1 == MP_TOKEN_OP_TILDE, "MP_TOKEN order changed, so Code::PythonTextArea::TokenColor might need to change too."); if (tokenKind >= MP_TOKEN_KW_FALSE && tokenKind <= MP_TOKEN_KW_YIELD) { return true; } static_assert(MP_TOKEN_OP_TILDE + 1 == MP_TOKEN_OP_LESS && MP_TOKEN_OP_LESS + 1 == MP_TOKEN_OP_MORE && MP_TOKEN_OP_MORE + 1 == MP_TOKEN_OP_DBL_EQUAL && MP_TOKEN_OP_DBL_EQUAL + 1 == MP_TOKEN_OP_LESS_EQUAL && MP_TOKEN_OP_LESS_EQUAL + 1 == MP_TOKEN_OP_MORE_EQUAL && MP_TOKEN_OP_MORE_EQUAL + 1 == MP_TOKEN_OP_NOT_EQUAL && MP_TOKEN_OP_NOT_EQUAL + 1 == MP_TOKEN_OP_PIPE && MP_TOKEN_OP_PIPE + 1 == MP_TOKEN_OP_CARET && MP_TOKEN_OP_CARET + 1 == MP_TOKEN_OP_AMPERSAND && MP_TOKEN_OP_AMPERSAND + 1 == MP_TOKEN_OP_DBL_LESS && MP_TOKEN_OP_DBL_LESS + 1 == MP_TOKEN_OP_DBL_MORE && MP_TOKEN_OP_DBL_MORE + 1 == MP_TOKEN_OP_PLUS && MP_TOKEN_OP_PLUS + 1 == MP_TOKEN_OP_MINUS && MP_TOKEN_OP_MINUS + 1 == MP_TOKEN_OP_STAR && MP_TOKEN_OP_STAR + 1 == MP_TOKEN_OP_AT && MP_TOKEN_OP_AT + 1 == MP_TOKEN_OP_DBL_SLASH && MP_TOKEN_OP_DBL_SLASH + 1 == MP_TOKEN_OP_SLASH && MP_TOKEN_OP_SLASH + 1 == MP_TOKEN_OP_PERCENT && MP_TOKEN_OP_PERCENT + 1 == MP_TOKEN_OP_DBL_STAR && MP_TOKEN_OP_DBL_STAR + 1 == MP_TOKEN_DEL_PIPE_EQUAL && MP_TOKEN_DEL_PIPE_EQUAL + 1 == MP_TOKEN_DEL_CARET_EQUAL && MP_TOKEN_DEL_CARET_EQUAL + 1 == MP_TOKEN_DEL_AMPERSAND_EQUAL && MP_TOKEN_DEL_AMPERSAND_EQUAL + 1 == MP_TOKEN_DEL_DBL_LESS_EQUAL && MP_TOKEN_DEL_DBL_LESS_EQUAL + 1 == MP_TOKEN_DEL_DBL_MORE_EQUAL && MP_TOKEN_DEL_DBL_MORE_EQUAL + 1 == MP_TOKEN_DEL_PLUS_EQUAL && MP_TOKEN_DEL_PLUS_EQUAL + 1 == MP_TOKEN_DEL_MINUS_EQUAL && MP_TOKEN_DEL_MINUS_EQUAL + 1 == MP_TOKEN_DEL_STAR_EQUAL && MP_TOKEN_DEL_STAR_EQUAL + 1 == MP_TOKEN_DEL_AT_EQUAL && MP_TOKEN_DEL_AT_EQUAL + 1 == MP_TOKEN_DEL_DBL_SLASH_EQUAL && MP_TOKEN_DEL_DBL_SLASH_EQUAL + 1 == MP_TOKEN_DEL_SLASH_EQUAL && MP_TOKEN_DEL_SLASH_EQUAL + 1 == MP_TOKEN_DEL_PERCENT_EQUAL && MP_TOKEN_DEL_PERCENT_EQUAL + 1 == MP_TOKEN_DEL_DBL_STAR_EQUAL && MP_TOKEN_DEL_DBL_STAR_EQUAL + 1 == MP_TOKEN_DEL_PAREN_OPEN && MP_TOKEN_DEL_PAREN_OPEN + 1 == MP_TOKEN_DEL_PAREN_CLOSE && MP_TOKEN_DEL_PAREN_CLOSE + 1 == MP_TOKEN_DEL_BRACKET_OPEN && MP_TOKEN_DEL_BRACKET_OPEN + 1 == MP_TOKEN_DEL_BRACKET_CLOSE && MP_TOKEN_DEL_BRACKET_CLOSE + 1 == MP_TOKEN_DEL_BRACE_OPEN && MP_TOKEN_DEL_BRACE_OPEN + 1 == MP_TOKEN_DEL_BRACE_CLOSE && MP_TOKEN_DEL_BRACE_CLOSE + 1 == MP_TOKEN_DEL_COMMA && MP_TOKEN_DEL_COMMA + 1 == MP_TOKEN_DEL_COLON && MP_TOKEN_DEL_COLON + 1 == MP_TOKEN_DEL_PERIOD && MP_TOKEN_DEL_PERIOD + 1 == MP_TOKEN_DEL_SEMICOLON && MP_TOKEN_DEL_SEMICOLON + 1 == MP_TOKEN_DEL_EQUAL && MP_TOKEN_DEL_EQUAL + 1 == MP_TOKEN_DEL_MINUS_MORE, "MP_TOKEN order changed, so Code::PythonTextArea::TokenColor might need to change too."); return false; } static inline KDColor TokenColor(mp_token_kind_t tokenKind) { if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { return Palette::CodeText; } if (tokenKind == MP_TOKEN_STRING) { return StringColor; } if (tokenKind == MP_TOKEN_INTEGER || tokenKind == MP_TOKEN_FLOAT_OR_IMAG) { return NumberColor; } static_assert(MP_TOKEN_ELLIPSIS + 1 == MP_TOKEN_KW_FALSE && MP_TOKEN_KW_FALSE + 1 == MP_TOKEN_KW_NONE && MP_TOKEN_KW_NONE + 1 == MP_TOKEN_KW_TRUE && MP_TOKEN_KW_TRUE + 1 == MP_TOKEN_KW___DEBUG__ && MP_TOKEN_KW___DEBUG__ + 1 == MP_TOKEN_KW_AND && MP_TOKEN_KW_AND + 1 == MP_TOKEN_KW_AS && MP_TOKEN_KW_AS + 1 == MP_TOKEN_KW_ASSERT /* Here there are keywords that depend on MICROPY_PY_ASYNC_AWAIT, we do * not test them */ && MP_TOKEN_KW_BREAK + 1 == MP_TOKEN_KW_CLASS && MP_TOKEN_KW_CLASS + 1 == MP_TOKEN_KW_CONTINUE && MP_TOKEN_KW_CONTINUE + 1 == MP_TOKEN_KW_DEF && MP_TOKEN_KW_DEF + 1 == MP_TOKEN_KW_DEL && MP_TOKEN_KW_DEL + 1 == MP_TOKEN_KW_ELIF && MP_TOKEN_KW_ELIF + 1 == MP_TOKEN_KW_ELSE && MP_TOKEN_KW_ELSE + 1 == MP_TOKEN_KW_EXCEPT && MP_TOKEN_KW_EXCEPT + 1 == MP_TOKEN_KW_FINALLY && MP_TOKEN_KW_FINALLY + 1 == MP_TOKEN_KW_FOR && MP_TOKEN_KW_FOR + 1 == MP_TOKEN_KW_FROM && MP_TOKEN_KW_FROM + 1 == MP_TOKEN_KW_GLOBAL && MP_TOKEN_KW_GLOBAL + 1 == MP_TOKEN_KW_IF && MP_TOKEN_KW_IF + 1 == MP_TOKEN_KW_IMPORT && MP_TOKEN_KW_IMPORT + 1 == MP_TOKEN_KW_IN && MP_TOKEN_KW_IN + 1 == MP_TOKEN_KW_IS && MP_TOKEN_KW_IS + 1 == MP_TOKEN_KW_LAMBDA && MP_TOKEN_KW_LAMBDA + 1 == MP_TOKEN_KW_NONLOCAL && MP_TOKEN_KW_NONLOCAL + 1 == MP_TOKEN_KW_NOT && MP_TOKEN_KW_NOT + 1 == MP_TOKEN_KW_OR && MP_TOKEN_KW_OR + 1 == MP_TOKEN_KW_PASS && MP_TOKEN_KW_PASS + 1 == MP_TOKEN_KW_RAISE && MP_TOKEN_KW_RAISE + 1 == MP_TOKEN_KW_RETURN && MP_TOKEN_KW_RETURN + 1 == MP_TOKEN_KW_TRY && MP_TOKEN_KW_TRY + 1 == MP_TOKEN_KW_WHILE && MP_TOKEN_KW_WHILE + 1 == MP_TOKEN_KW_WITH && MP_TOKEN_KW_WITH + 1 == MP_TOKEN_KW_YIELD && MP_TOKEN_KW_YIELD + 1 == MP_TOKEN_OP_ASSIGN && MP_TOKEN_OP_ASSIGN + 1 == MP_TOKEN_OP_TILDE, "MP_TOKEN order changed, so Code::PythonTextArea::TokenColor might need to change too."); if (tokenKind >= MP_TOKEN_KW_FALSE && tokenKind <= MP_TOKEN_KW_YIELD) { return KeywordColor; } static_assert(MP_TOKEN_OP_TILDE + 1 == MP_TOKEN_OP_LESS && MP_TOKEN_OP_LESS + 1 == MP_TOKEN_OP_MORE && MP_TOKEN_OP_MORE + 1 == MP_TOKEN_OP_DBL_EQUAL && MP_TOKEN_OP_DBL_EQUAL + 1 == MP_TOKEN_OP_LESS_EQUAL && MP_TOKEN_OP_LESS_EQUAL + 1 == MP_TOKEN_OP_MORE_EQUAL && MP_TOKEN_OP_MORE_EQUAL + 1 == MP_TOKEN_OP_NOT_EQUAL && MP_TOKEN_OP_NOT_EQUAL + 1 == MP_TOKEN_OP_PIPE && MP_TOKEN_OP_PIPE + 1 == MP_TOKEN_OP_CARET && MP_TOKEN_OP_CARET + 1 == MP_TOKEN_OP_AMPERSAND && MP_TOKEN_OP_AMPERSAND + 1 == MP_TOKEN_OP_DBL_LESS && MP_TOKEN_OP_DBL_LESS + 1 == MP_TOKEN_OP_DBL_MORE && MP_TOKEN_OP_DBL_MORE + 1 == MP_TOKEN_OP_PLUS && MP_TOKEN_OP_PLUS + 1 == MP_TOKEN_OP_MINUS && MP_TOKEN_OP_MINUS + 1 == MP_TOKEN_OP_STAR && MP_TOKEN_OP_STAR + 1 == MP_TOKEN_OP_AT && MP_TOKEN_OP_AT + 1 == MP_TOKEN_OP_DBL_SLASH && MP_TOKEN_OP_DBL_SLASH + 1 == MP_TOKEN_OP_SLASH && MP_TOKEN_OP_SLASH + 1 == MP_TOKEN_OP_PERCENT && MP_TOKEN_OP_PERCENT + 1 == MP_TOKEN_OP_DBL_STAR && MP_TOKEN_OP_DBL_STAR + 1 == MP_TOKEN_DEL_PIPE_EQUAL && MP_TOKEN_DEL_PIPE_EQUAL + 1 == MP_TOKEN_DEL_CARET_EQUAL && MP_TOKEN_DEL_CARET_EQUAL + 1 == MP_TOKEN_DEL_AMPERSAND_EQUAL && MP_TOKEN_DEL_AMPERSAND_EQUAL + 1 == MP_TOKEN_DEL_DBL_LESS_EQUAL && MP_TOKEN_DEL_DBL_LESS_EQUAL + 1 == MP_TOKEN_DEL_DBL_MORE_EQUAL && MP_TOKEN_DEL_DBL_MORE_EQUAL + 1 == MP_TOKEN_DEL_PLUS_EQUAL && MP_TOKEN_DEL_PLUS_EQUAL + 1 == MP_TOKEN_DEL_MINUS_EQUAL && MP_TOKEN_DEL_MINUS_EQUAL + 1 == MP_TOKEN_DEL_STAR_EQUAL && MP_TOKEN_DEL_STAR_EQUAL + 1 == MP_TOKEN_DEL_AT_EQUAL && MP_TOKEN_DEL_AT_EQUAL + 1 == MP_TOKEN_DEL_DBL_SLASH_EQUAL && MP_TOKEN_DEL_DBL_SLASH_EQUAL + 1 == MP_TOKEN_DEL_SLASH_EQUAL && MP_TOKEN_DEL_SLASH_EQUAL + 1 == MP_TOKEN_DEL_PERCENT_EQUAL && MP_TOKEN_DEL_PERCENT_EQUAL + 1 == MP_TOKEN_DEL_DBL_STAR_EQUAL && MP_TOKEN_DEL_DBL_STAR_EQUAL + 1 == MP_TOKEN_DEL_PAREN_OPEN && MP_TOKEN_DEL_PAREN_OPEN + 1 == MP_TOKEN_DEL_PAREN_CLOSE && MP_TOKEN_DEL_PAREN_CLOSE + 1 == MP_TOKEN_DEL_BRACKET_OPEN && MP_TOKEN_DEL_BRACKET_OPEN + 1 == MP_TOKEN_DEL_BRACKET_CLOSE && MP_TOKEN_DEL_BRACKET_CLOSE + 1 == MP_TOKEN_DEL_BRACE_OPEN && MP_TOKEN_DEL_BRACE_OPEN + 1 == MP_TOKEN_DEL_BRACE_CLOSE && MP_TOKEN_DEL_BRACE_CLOSE + 1 == MP_TOKEN_DEL_COMMA && MP_TOKEN_DEL_COMMA + 1 == MP_TOKEN_DEL_COLON && MP_TOKEN_DEL_COLON + 1 == MP_TOKEN_DEL_PERIOD && MP_TOKEN_DEL_PERIOD + 1 == MP_TOKEN_DEL_SEMICOLON && MP_TOKEN_DEL_SEMICOLON + 1 == MP_TOKEN_DEL_EQUAL && MP_TOKEN_DEL_EQUAL + 1 == MP_TOKEN_DEL_MINUS_MORE, "MP_TOKEN order changed, so Code::PythonTextArea::TokenColor might need to change too."); if ((tokenKind >= MP_TOKEN_OP_TILDE && tokenKind <= MP_TOKEN_DEL_DBL_STAR_EQUAL) || tokenKind == MP_TOKEN_DEL_EQUAL || tokenKind == MP_TOKEN_DEL_MINUS_MORE || tokenKind == MP_TOKEN_OP_ASSIGN) { return OperatorColor; } return Palette::CodeText; } static inline size_t TokenLength(mp_lexer_t * lex, const char * tokenPosition) { /* The lexer stores the beginning of the current token and of the next token, * so we just use that. */ if (lex->line > 1) { /* The next token is on the next line, so we cannot just make the difference * of the columns. */ return UTF8Helper::CodePointSearch(tokenPosition, '\n') - tokenPosition; } return lex->column - lex->tok_column; } PythonTextArea::AutocompletionType PythonTextArea::autocompletionType(const char * autocompletionLocation, const char ** autocompletionLocationBeginning, const char ** autocompletionLocationEnd) const { const char * location = autocompletionLocation != nullptr ? autocompletionLocation : cursorLocation(); const char * beginningOfToken = nullptr; /* If there is already autocompleting, the cursor must be at the end of an * identifier. Trying to compute autocompletionType will fail: because of the * autocompletion text, the cursor seems to be in the middle of an identifier. */ AutocompletionType autocompleteType = isAutocompleting() ? AutocompletionType::EndOfIdentifier : AutocompletionType::NoIdentifier; if (autocompletionLocationBeginning == nullptr && autocompletionLocationEnd == nullptr) { return autocompleteType; } 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 && currentTokenKind != MP_TOKEN_FSTRING_RAW) { tokenStart = firstNonSpace + lex->tok_column - 1; tokenEnd = tokenStart + TokenLength(lex, tokenStart); if (location < tokenStart) { // The location for autocompletion is not in an identifier assert(autocompleteType == AutocompletionType::NoIdentifier); break; } if (location <= tokenEnd) { if (currentTokenKind == MP_TOKEN_NAME || (currentTokenKind >= MP_TOKEN_KW_FALSE && currentTokenKind <= MP_TOKEN_KW_YIELD)) { /* The location for autocompletion is in the middle or at the end of * an identifier. */ beginningOfToken = tokenStart; /* If autocompleteType is already EndOfIdentifier, we are * autocompleting, so we do not need to update autocompleteType. If we * recomputed autocompleteType now, we might wrongly think that it is * MiddleOfIdentifier because of the autocompetion text. * Example : fin|ally -> the lexer is at the end of "fin", but because * we are autocompleting with "ally", the lexer thinks the cursor is * in the middle of an identifier. */ if (autocompleteType != AutocompletionType::EndOfIdentifier) { autocompleteType = location < tokenEnd ? AutocompletionType::MiddleOfIdentifier : AutocompletionType::EndOfIdentifier; } } 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 { return UTF8Helper::BeginningOfWord(editedText(), cursorLocation()); } void PythonTextArea::ContentView::loadSyntaxHighlighter() { m_pythonDelegate->initPythonWithUser(this); } void PythonTextArea::ContentView::unloadSyntaxHighlighter() { m_pythonDelegate->deinitPython(); } void PythonTextArea::ContentView::clearRect(KDContext * ctx, KDRect rect) const { ctx->fillRect(rect, BackgroundColor); } #define LOG_DRAWING 0 #if LOG_DRAWING #include #define LOG_DRAW(...) printf(__VA_ARGS__) #else #define LOG_DRAW(...) #endif void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char * text, size_t byteLength, int fromColumn, int toColumn, const char * selectionStart, const char * selectionEnd) const { LOG_DRAW("Drawing \"%.*s\"\n", byteLength, text); assert(m_pythonDelegate->isPythonUser(this)); /* We're using the MicroPython lexer to do syntax highlighting on a per-line * basis. This can work, however the MicroPython lexer won't accept a line * starting with a whitespace. So we're discarding leading whitespaces * beforehand. */ const char * firstNonSpace = UTF8Helper::NotCodePointSearch(text, ' '); if (firstNonSpace != text) { // Color the discarded leading whitespaces const char * spacesStart = UTF8Helper::CodePointAtGlyphOffset(text, fromColumn); drawStringAt( ctx, line, fromColumn, spacesStart, std::min(text + byteLength, firstNonSpace) - spacesStart, StringColor, BackgroundColor, selectionStart, selectionEnd, HighlightColor, false); } if (UTF8Helper::CodePointIs(firstNonSpace, UCodePointNull)) { return; } const char * autocompleteStart = m_autocomplete ? m_cursorLocation : nullptr; nlr_buf_t nlr; if (nlr_push(&nlr) == 0) { mp_lexer_t * lex = mp_lexer_new_from_str_len(0, firstNonSpace, byteLength - (firstNonSpace - text), 0); LOG_DRAW("Pop token %d\n", lex->tok_kind); const char * tokenFrom = firstNonSpace; size_t tokenLength = 0; const char * tokenEnd = firstNonSpace; while (lex->tok_kind != MP_TOKEN_NEWLINE && lex->tok_kind != MP_TOKEN_END && lex->tok_kind != MP_TOKEN_FSTRING_RAW) { tokenFrom = firstNonSpace + lex->tok_column - 1; if (tokenFrom != tokenEnd) { // We passed over white spaces, we need to color them drawStringAt( ctx, line, UTF8Helper::GlyphOffsetAtCodePoint(text, tokenEnd), tokenEnd, std::min(text + byteLength, tokenFrom) - tokenEnd, StringColor, BackgroundColor, selectionStart, selectionEnd, HighlightColor, false); } tokenLength = TokenLength(lex, tokenFrom); tokenEnd = tokenFrom + tokenLength; // If the token is being autocompleted, use DefaultColor/Font KDColor color = (tokenFrom <= autocompleteStart && autocompleteStart < tokenEnd) ? Palette::CodeText : TokenColor(lex->tok_kind); bool font = (tokenFrom <= autocompleteStart && autocompleteStart < tokenEnd) ? false:isItalic(lex->tok_kind); LOG_DRAW("Draw \"%.*s\" for token %d\n", tokenLength, tokenFrom, lex->tok_kind); drawStringAt(ctx, line, UTF8Helper::GlyphOffsetAtCodePoint(text, tokenFrom), tokenFrom, tokenLength, color, BackgroundColor, selectionStart, selectionEnd, HighlightColor, font); mp_lexer_to_next(lex); LOG_DRAW("Pop token %d\n", lex->tok_kind); } tokenFrom += tokenLength; KDColor color = CommentColor; if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { color = Palette::CodeText; } // Even if the token is being autocompleted, use CommentColor if (tokenFrom < text + byteLength) { LOG_DRAW("Draw comment \"%.*s\" from %d\n", byteLength - (tokenFrom - text), firstNonSpace, tokenFrom); drawStringAt(ctx, line, UTF8Helper::GlyphOffsetAtCodePoint(text, tokenFrom), tokenFrom, text + byteLength - tokenFrom, color, BackgroundColor, selectionStart, selectionEnd, HighlightColor, true); } mp_lexer_free(lex); nlr_pop(); } // Redraw the autocompleted word in the right color if (m_autocomplete && autocompleteStart >= text && autocompleteStart < text + byteLength) { assert(m_autocompletionEnd != nullptr && m_autocompletionEnd > autocompleteStart); drawStringAt( ctx, line, UTF8Helper::GlyphOffsetAtCodePoint(text, autocompleteStart), autocompleteStart, std::min(text + byteLength, m_autocompletionEnd) - autocompleteStart, AutocompleteColor, BackgroundColor, nullptr, nullptr, HighlightColor, false); } } KDRect PythonTextArea::ContentView::dirtyRectFromPosition(const char * position, bool includeFollowingLines) const { /* Mark the whole line as dirty. * TextArea has a very conservative approach and only dirties the surroundings * of the current character. That works for plain text, but when doing syntax * highlighting, you may want to redraw the surroundings as well. For example, * if editing "def foo" into "df foo", you'll want to redraw "df". */ KDRect baseDirtyRect = TextArea::ContentView::dirtyRectFromPosition(position, includeFollowingLines); return KDRect( bounds().x(), baseDirtyRect.y(), bounds().width(), baseDirtyRect.height() ); } bool PythonTextArea::handleEvent(Ion::Events::Event event) { if (m_contentView.isAutocompleting()) { // Handle event with autocompletion if (event == Ion::Events::Right || event == Ion::Events::ShiftRight || event == Ion::Events::OK) { m_contentView.reloadRectFromPosition(m_contentView.cursorLocation(), false); acceptAutocompletion(event != Ion::Events::ShiftRight); if (event != Ion::Events::ShiftRight) { // Do not process the event more scrollToCursor(); return true; } } else if (event == Ion::Events::Toolbox || event == Ion::Events::Var || event == Ion::Events::Shift || event == Ion::Events::Alpha || event == Ion::Events::OnOff) { } else if(event == Ion::Events::Up || event == Ion::Events::Down) { cycleAutocompletion(event == Ion::Events::Down); return true; } else { removeAutocompletion(); m_contentView.reloadRectFromPosition(m_contentView.cursorLocation(), false); if (event == Ion::Events::Back) { // Do not process the event more return true; } } } bool result = TextArea::handleEvent(event); if (event == Ion::Events::Backspace && !m_contentView.isAutocompleting() && selectionIsEmpty()) { /* We want to add autocompletion when we are editing a word (after adding or * deleting text). So if nothing is selected, we add the autocompletion if * the event is backspace, as autocompletion has already been added if the * event added text, in handleEventWithText. */ addAutocompletion(); } return result; } bool PythonTextArea::handleEventWithText(const char * text, bool indentation, bool forceCursorRightOfText) { if (*text == 0) { return false; } if (m_contentView.isAutocompleting()) { removeAutocompletion(); } bool result = TextArea::handleEventWithText(text, indentation, forceCursorRightOfText); addAutocompletion(); return result; } void PythonTextArea::removeAutocompletion() { assert(m_contentView.isAutocompleting()); removeAutocompletionText(); m_contentView.setAutocompleting(false); } void PythonTextArea::removeAutocompletionText() { assert(m_contentView.isAutocompleting()); assert(m_contentView.autocompletionEnd() != nullptr); const char * autocompleteStart = m_contentView.cursorLocation(); const char * autocompleteEnd = m_contentView.autocompletionEnd(); assert(autocompleteEnd != nullptr && autocompleteEnd > autocompleteStart); m_contentView.removeText(autocompleteStart, autocompleteEnd); } void PythonTextArea::addAutocompletion() { assert(!m_contentView.isAutocompleting()); const char * autocompletionTokenBeginning = nullptr; const char * autocompletionLocation = const_cast(cursorLocation()); m_autocompletionResultIndex = 0; if (autocompletionType(autocompletionLocation, &autocompletionTokenBeginning) != AutocompletionType::EndOfIdentifier) { // The cursor is not at the end of an identifier. return; } // First load variables and functions that complete the textToAutocomplete const int scriptIndex = m_contentView.pythonDelegate()->menuController()->editedScriptIndex(); m_contentView.pythonDelegate()->variableBoxController()->loadFunctionsAndVariables(scriptIndex, autocompletionTokenBeginning, autocompletionLocation - autocompletionTokenBeginning); addAutocompletionTextAtIndex(0); } bool PythonTextArea::addAutocompletionTextAtIndex(int nextIndex, int * currentIndexToUpdate) { // If Autocomplete disable, skip this step if(!GlobalPreferences::sharedGlobalPreferences()->autocomplete()) { return false; } // The variable box should be loaded at this point const char * autocompletionTokenBeginning = nullptr; const char * autocompletionLocation = const_cast(cursorLocation()); AutocompletionType type = autocompletionType(autocompletionLocation, &autocompletionTokenBeginning); // Done to get autocompletionTokenBeginning assert(type == AutocompletionType::EndOfIdentifier); (void)type; // Silence warnings VariableBoxController * varBox = m_contentView.pythonDelegate()->variableBoxController(); int textToInsertLength = 0; bool addParentheses = false; const char * textToInsert = varBox->autocompletionAlternativeAtIndex(autocompletionLocation - autocompletionTokenBeginning, &textToInsertLength, &addParentheses, nextIndex, currentIndexToUpdate); if (textToInsert == nullptr) { return false; } if (textToInsertLength > 0) { // Try to insert the text (this might fail if the buffer is full) if (!m_contentView.insertTextAtLocation(textToInsert, const_cast(autocompletionLocation), textToInsertLength)) { return false; } autocompletionLocation += textToInsertLength; m_contentView.setAutocompleting(true); m_contentView.setAutocompletionEnd(autocompletionLocation); } // Try to insert the parentheses if needed const char * parentheses = ScriptNodeCell::k_parentheses; constexpr int parenthesesLength = 2; assert(strlen(parentheses) == parenthesesLength); /* If couldInsertText is false, we should not try to add the parentheses as * there was already not enough space to add the autocompletion. */ if (addParentheses && m_contentView.insertTextAtLocation(parentheses, const_cast(autocompletionLocation), parenthesesLength)) { m_contentView.setAutocompleting(true); m_contentView.setAutocompletionEnd(autocompletionLocation + parenthesesLength); return true; } return (textToInsertLength > 0); } void PythonTextArea::cycleAutocompletion(bool downwards) { assert(m_contentView.isAutocompleting()); removeAutocompletionText(); addAutocompletionTextAtIndex(m_autocompletionResultIndex + (downwards ? 1 : -1), &m_autocompletionResultIndex); } void PythonTextArea::acceptAutocompletion(bool moveCursorToEndOfAutocompletion) { assert(m_contentView.isAutocompleting()); // Save the cursor location const char * previousCursorLocation = cursorLocation(); removeAutocompletion(); m_contentView.pythonDelegate()->variableBoxController()->setSender(this); m_contentView.pythonDelegate()->variableBoxController()->insertAutocompletionResultAtIndex(m_autocompletionResultIndex); // insertAutocompletionResultAtIndex already added the autocompletion // If we did not want to move the cursor, restore its position. if (!moveCursorToEndOfAutocompletion) { setCursorLocation(previousCursorLocation); } } }