From 29cacbc44fa66c4266401762f6b3aa7970621d91 Mon Sep 17 00:00:00 2001 From: Romain Goyet Date: Mon, 4 Jun 2018 14:09:20 +0200 Subject: [PATCH] [code] Implement syntax highlighting in PythonTextArea --- apps/code/python_text_area.cpp | 179 +++++++++++++++++++++++++-------- apps/code/python_text_area.h | 6 +- 2 files changed, 142 insertions(+), 43 deletions(-) diff --git a/apps/code/python_text_area.cpp b/apps/code/python_text_area.cpp index a9ecbd802..840c0c39c 100644 --- a/apps/code/python_text_area.cpp +++ b/apps/code/python_text_area.cpp @@ -1,53 +1,150 @@ #include "python_text_area.h" +extern "C" { +#include "py/nlr.h" +#include "py/lexer.h" +} +#include + + namespace Code { -/* PythonTextArea */ +constexpr KDColor CommentColor = KDColor::RGB24(0x999988); +constexpr KDColor NumberColor = KDColor::RGB24(0x009999); +constexpr KDColor KeywordColor = KDColor::RGB24(0xFF000C); +// constexpr KDColor BuiltinColor = KDColor::RGB24(0x0086B3); +constexpr KDColor OperatorColor = KDColor::RGB24(0xd73a49); +constexpr KDColor StringColor = KDColor::RGB24(0x032f62); +constexpr KDColor BackgroundColor = KDColorWhite; -void PythonTextArea::ContentView::drawRect(KDContext * ctx, KDRect rect) const { +static inline KDColor TokenColor(mp_token_kind_t tokenKind) { + if (tokenKind == MP_TOKEN_STRING) { + return StringColor; + } + if (tokenKind == MP_TOKEN_INTEGER || tokenKind == MP_TOKEN_FLOAT_OR_IMAG) { + return NumberColor; + } + if (tokenKind >= MP_TOKEN_KW_FALSE && tokenKind <= MP_TOKEN_KW_YIELD) { + return KeywordColor; + } + if (tokenKind >= MP_TOKEN_OP_PLUS && tokenKind <= MP_TOKEN_OP_NOT_EQUAL) { + return OperatorColor; + } + return KDColorBlack; } -/* PythonTextArea::ContentView */ - -PythonTextArea::ContentView::ContentView(KDText::FontSize fontSize) : - TextArea::ContentView(fontSize) -{ -} - -void PythonTextArea::ContentView::drawRect(KDContext * ctx, KDRect rect) const { - ctx->fillRect(rect, m_backgroundColor); - - KDSize charSize = KDText::charSize(m_fontSize); - - // We want to draw even partially visible characters. So we need to round - // down for the top left corner and up for the bottom right one. - Text::Position topLeft( - rect.x()/charSize.width(), - rect.y()/charSize.height() - ); - Text::Position bottomRight( - rect.right()/charSize.width() + 1, - rect.bottom()/charSize.height() + 1 - ); - - int y = 0; - size_t x = topLeft.column(); - - for (Text::Line line : m_text) { - if (y >= topLeft.line() && y <= bottomRight.line() && topLeft.column() < (int)line.length()) { - //drawString(line.text(), 0, y*charHeight); // Naive version - ctx->drawString( - line.text() + topLeft.column(), - KDPoint(x*charSize.width(), y*charSize.height()), - m_fontSize, - m_textColor, - m_backgroundColor, - min(line.length() - topLeft.column(), bottomRight.column() - topLeft.column()) - ); - } - y++; +static inline int TokenLength(mp_lexer_t * lex) { + if (lex->tok_kind == MP_TOKEN_STRING) { + return lex->vstr.len + 2; + } + if (lex->vstr.len > 0) { + return lex->vstr.len; + } + switch (lex->tok_kind) { + case MP_TOKEN_OP_DBL_STAR: + case MP_TOKEN_OP_DBL_SLASH: + case MP_TOKEN_OP_DBL_LESS: + case MP_TOKEN_OP_DBL_MORE: + case MP_TOKEN_OP_LESS_EQUAL: + case MP_TOKEN_OP_MORE_EQUAL: + case MP_TOKEN_OP_DBL_EQUAL: + return 2; + case MP_TOKEN_DEL_DBL_MORE_EQUAL: + case MP_TOKEN_DEL_DBL_LESS_EQUAL: + case MP_TOKEN_DEL_DBL_STAR_EQUAL: + return 3; + default: + return 1; } } +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 length, int fromColumn, int toColumn) const { + LOG_DRAW("Drawing \"%.*s\"\n", length, text); + + char m_pythonHeap[4096]; + + MicroPython::init(m_pythonHeap, m_pythonHeap + 4096); + + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + /* 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. */ + int whitespaceOffset = 0; + while (text[whitespaceOffset] == ' ' && whitespaceOffset < length) { + whitespaceOffset++; + } + + mp_lexer_t * lex = mp_lexer_new_from_str_len(0, text + whitespaceOffset, length - whitespaceOffset, 0); + LOG_DRAW("Pop token %d\n", lex->tok_kind); + + int tokenFrom = 0; + int tokenLength = 0; + KDColor tokenColor = KDColorBlack; + + while (lex->tok_kind != MP_TOKEN_NEWLINE && lex->tok_kind != MP_TOKEN_END) { + tokenFrom = whitespaceOffset + lex->tok_column - 1; + tokenLength = TokenLength(lex); + tokenColor = TokenColor(lex->tok_kind); + + LOG_DRAW("Draw \"%.*s\" for token %d\n", tokenLength, text + tokenFrom, lex->tok_kind); + drawStringAt(ctx, line, + tokenFrom, + text + tokenFrom, // text + tokenLength, // length + tokenColor, + KDColorWhite + ); + + mp_lexer_to_next(lex); + LOG_DRAW("Pop token %d\n", lex->tok_kind); + } + + tokenFrom = tokenFrom + tokenLength; + if (tokenFrom != length) { + LOG_DRAW("Draw comment \"%.*s\" from %d\n", length - tokenFrom, text + tokenFrom, tokenFrom); + drawStringAt(ctx, line, + tokenFrom, + text + tokenFrom, // text + length - tokenFrom, // length + CommentColor, + KDColorWhite + ); + } + + mp_lexer_free(lex); + nlr_pop(); + } + + MicroPython::deinit(); +} + +KDRect PythonTextArea::ContentView::dirtyRectFromCursorPosition(size_t index, bool lineBreak) 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::dirtyRectFromCursorPosition(index, lineBreak); + return KDRect( + bounds().x(), + baseDirtyRect.y(), + bounds().width(), + baseDirtyRect.height() + ); +} } diff --git a/apps/code/python_text_area.h b/apps/code/python_text_area.h index 47f4b3b55..8b727bd4a 100644 --- a/apps/code/python_text_area.h +++ b/apps/code/python_text_area.h @@ -15,8 +15,10 @@ public: protected: class ContentView : public TextArea::ContentView { public: - ContentView(KDText::FontSize fontSize); - void drawRect(KDContext * ctx, KDRect rect) const override; + ContentView(KDText::FontSize fontSize) : TextArea::ContentView(fontSize) {} + void clearRect(KDContext * ctx, KDRect rect) const override; + void drawLine(KDContext * ctx, int line, const char * text, size_t length, int fromColumn, int toColumn) const override; + KDRect dirtyRectFromCursorPosition(size_t index, bool lineBreak) const override; }; private: const ContentView * nonEditableContentView() const override { return &m_contentView; }