diff --git a/apps/code/Makefile b/apps/code/Makefile index af50725cd59..7a70c3a04a0 100644 --- a/apps/code/Makefile +++ b/apps/code/Makefile @@ -18,6 +18,7 @@ app_code_src = $(addprefix apps/code/,\ script_name_cell.cpp \ script_parameter_controller.cpp \ toolbox_ion_keys.cpp \ + brackets.c \ ) app_code_test_src = $(addprefix apps/code/,\ diff --git a/apps/code/brackets.c b/apps/code/brackets.c new file mode 100644 index 00000000000..c6e4b4f3d5c --- /dev/null +++ b/apps/code/brackets.c @@ -0,0 +1,103 @@ +#include "brackets.h" + +void initTable(DynamicTable* table) { + table->size = 0; + table->capacity = 10; + table->positions = (int*)malloc(table->capacity * sizeof(int)); +} + +void initStaticTable(StaticTable* table) { + table->size = 0; +} + +void addToTable(DynamicTable* table, int position) { + if (table->size == table->capacity) { + table->capacity *= 2; + table->positions = (int*)realloc(table->positions, table->capacity * sizeof(int)); + } + table->positions[table->size++] = position; +} + + +void addToStaticTable(StaticTable* table, int position) { + if (table->size < MAX_PARENTHESES) { + table->positions[table->size++] = position; + } +} +bool isInTable(DynamicTable* table, int position) { + for (int i = 0; i < table->size; i++) { + if (table->positions[i] == position) { + return true; + } + } + return false; +} + +bool isInStaticTable(StaticTable* table, int position) { + for (int i = 0; i < table->size; i++) { + if (table->positions[i] == position) { + return true; + } + } + return false; +} + +void freeTable(DynamicTable* mismatches) { + free(mismatches->positions); +} + +int getBracketBalance(const char* str) { + int counter = 0; + for (int i = 0; str[i] != '\0' && str[i] != '\n'; i++) { + if ( + str[i] =='(' || + str[i] =='[' || + str[i] =='{' + ) { + counter++; + } + else if ( + str[i] ==')' || + str[i] ==']' || + str[i] =='}' + ) { + counter--; + } + } + return counter; +} + + +void findMismatchedParentheses(const char* str, StaticTable* mismatches) { + DynamicTable stack; + initTable(&stack); + for (int i = 0; str[i] != '\0'; i++) { + if (str[i] == '(' || str[i] == '{' || str[i] == '[') { + addToTable(&stack, i); + } else if (str[i] == ')' || str[i] == '}' || str[i] == ']') { + if (stack.size == 0) { + addToStaticTable(mismatches, i); + } else { + char open = str[stack.positions[stack.size - 1]]; + if ((str[i] == ')' && open == '(') || + (str[i] == '}' && open == '{') || + (str[i] == ']' && open == '[')) { // Matching Pair + stack.size--; + } else { + addToStaticTable(mismatches, i); + + } + } + } + } + + while (stack.size > 0) { + addToStaticTable(mismatches, stack.positions[--stack.size]); + } + + free(stack.positions); + +} + + + diff --git a/apps/code/brackets.h b/apps/code/brackets.h new file mode 100644 index 00000000000..4d5f90bc69b --- /dev/null +++ b/apps/code/brackets.h @@ -0,0 +1,30 @@ +#ifndef PYTHON_TEXT_AREA_BRACKETS +#define PYTHON_TEXT_AREA_BRACKETS +#define MAX_PARENTHESES 1000 +#include +#include +#include +#include + +typedef struct { + int* positions; + int size; + int capacity; +} DynamicTable; + +typedef struct { + int positions[MAX_PARENTHESES]; + int size; +} StaticTable; + +void initTable(DynamicTable* table); +void initStaticTable(StaticTable* table); +void addToTable(DynamicTable* table, int position); +void addToStaticTable(StaticTable* table, int position); +bool isInTable(DynamicTable* table, int position); +bool isInStaticTable(StaticTable* table, int position); +void freeTable(DynamicTable* mismatches); +int getBracketBalance(const char* str); +void findMismatchedParentheses(const char* str, StaticTable* mismatches); + +#endif \ No newline at end of file diff --git a/apps/code/python_text_area.cpp b/apps/code/python_text_area.cpp index 3edf9eeae8f..f2b2531b53b 100644 --- a/apps/code/python_text_area.cpp +++ b/apps/code/python_text_area.cpp @@ -4,7 +4,7 @@ #include #include #include "../global_preferences.h" - +#include extern "C" { #include "py/nlr.h" #include "py/lexer.h" @@ -23,6 +23,11 @@ constexpr KDColor StringColor = Palette::CodeString; constexpr KDColor BackgroundColor = Palette::CodeBackground; constexpr KDColor HighlightColor = Palette::CodeBackgroundSelected; constexpr KDColor AutocompleteColor = KDColor::RGB24(0xC6C6C6); // TODO Palette change +constexpr KDColor matchingParenthesisColors[3] = { // TODO Palette change + KDColor::RGB24(0x0431FA), + KDColor::RGB24(0x319331), + KDColor::RGB24(0x7B3814) + }; bool isItalic(mp_token_kind_t tokenKind) { if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { @@ -34,6 +39,30 @@ bool isItalic(mp_token_kind_t tokenKind) { return false; } +bool isOpeningBracket(mp_token_kind_t tokenKind) { + if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { + return false; + } + if (tokenKind == MP_TOKEN_DEL_PAREN_OPEN || + tokenKind == MP_TOKEN_DEL_BRACE_OPEN || + tokenKind == MP_TOKEN_DEL_BRACKET_OPEN) { + return true; + } + return false; +} + +bool isClosingBracket(mp_token_kind_t tokenKind) { + if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { + return false; + } + if (tokenKind == MP_TOKEN_DEL_PAREN_CLOSE || + tokenKind == MP_TOKEN_DEL_BRACE_CLOSE || + tokenKind == MP_TOKEN_DEL_BRACKET_CLOSE) { + return true; + } + return false; +} + static inline KDColor TokenColor(mp_token_kind_t tokenKind) { if (!GlobalPreferences::sharedGlobalPreferences()->syntaxhighlighting()) { return Palette::CodeText; @@ -242,16 +271,17 @@ void PythonTextArea::ContentView::clearRect(KDContext * ctx, KDRect rect) const #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 { +void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char * text, size_t byteLength, int fromColumn, int toColumn, const char * selectionStart, const char * selectionEnd, StaticTable* mismatchedParenthesesPositions, int charBefore, int bracketBalance) 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, ' '); + int numberOfLeadingSpaces = strlen(text) - strlen(firstNonSpace); if (firstNonSpace != text) { // Color the discarded leading whitespaces const char * spacesStart = UTF8Helper::CodePointAtGlyphOffset(text, fromColumn); @@ -282,6 +312,9 @@ void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char const char * tokenFrom = firstNonSpace; size_t tokenLength = 0; const char * tokenEnd = firstNonSpace; + + int bracketLineBalance = 0; + int currentPosition = charBefore + numberOfLeadingSpaces; 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) { @@ -298,6 +331,7 @@ void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char selectionEnd, HighlightColor, false); + currentPosition += 1; // Add the number of space } tokenLength = TokenLength(lex, tokenFrom); tokenEnd = tokenFrom + tokenLength; @@ -305,7 +339,25 @@ void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char // If the token is being autocompleted, use DefaultColor/Font KDColor color = (tokenFrom <= autocompleteStart && autocompleteStart < tokenEnd) ? Palette::CodeText : TokenColor(lex->tok_kind); bool italic = (tokenFrom <= autocompleteStart && autocompleteStart < tokenEnd) ? false : isItalic(lex->tok_kind); - + + bool mismatched = isInStaticTable(mismatchedParenthesesPositions, currentPosition); + + // TODO: don't count Parentheses in a comment + if (isOpeningBracket(lex->tok_kind)) { + if (mismatched) color = KDColor::RGB24(0xFF0000); + else { + color = matchingParenthesisColors[(bracketLineBalance + bracketBalance)% 3]; + bracketLineBalance++; + } + } else if (isClosingBracket(lex->tok_kind)) { + if (mismatched) color = KDColor::RGB24(0xFF0000); + else { + bracketLineBalance--; + color = matchingParenthesisColors[(bracketLineBalance + bracketBalance)% 3]; + } + } + + LOG_DRAW("Draw \"%.*s\" for token %d\n", tokenLength, tokenFrom, lex->tok_kind); drawStringAt(ctx, line, UTF8Helper::GlyphOffsetAtCodePoint(text, tokenFrom), @@ -321,8 +373,9 @@ void PythonTextArea::ContentView::drawLine(KDContext * ctx, int line, const char mp_lexer_to_next(lex); LOG_DRAW("Pop token %d\n", lex->tok_kind); + currentPosition = currentPosition + 1; } - + tokenFrom += tokenLength; KDColor color = CommentColor; diff --git a/apps/code/python_text_area.h b/apps/code/python_text_area.h index 57a46a5f85c..5bdb26b8a2e 100644 --- a/apps/code/python_text_area.h +++ b/apps/code/python_text_area.h @@ -52,7 +52,7 @@ class PythonTextArea : public TextArea { void loadSyntaxHighlighter(); void unloadSyntaxHighlighter(); 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 char * selectionStart, const char * selectionEnd) const override; + void drawLine(KDContext * ctx, int line, const char * text, size_t length, int fromColumn, int toColumn, const char * selectionStart, const char * selectionEnd, StaticTable* mismatchedParenthesesPositions, int charBefore, int bracketBalance) const override; KDRect dirtyRectFromPosition(const char * position, bool includeFollowingLines) const override; private: App * m_pythonDelegate; diff --git a/escher/include/escher/text_area.h b/escher/include/escher/text_area.h index 110268b3f94..36d04fdd705 100644 --- a/escher/include/escher/text_area.h +++ b/escher/include/escher/text_area.h @@ -6,7 +6,9 @@ #include #include #include - +extern "C" { + #include +} // See TODO in EditableField class TextArea : public TextInput, public InputEventHandler { @@ -112,7 +114,7 @@ class TextArea : public TextInput, public InputEventHandler { } void drawRect(KDContext * ctx, KDRect rect) const override; void drawStringAt(KDContext * ctx, int line, int column, const char * text, int length, KDColor textColor, KDColor backgroundColor, const char * selectionStart, const char * selectionEnd, KDColor backgroundHighlightColor, bool isItalic = false) const; - virtual void drawLine(KDContext * ctx, int line, const char * text, size_t length, int fromColumn, int toColumn, const char * selectionStart, const char * selectionEnd) const = 0; + virtual void drawLine(KDContext * ctx, int line, const char * text, size_t length, int fromColumn, int toColumn, const char * selectionStart, const char * selectionEnd, StaticTable* mismatchedParenthesesPositions, int charBefore, int bracketBalance) const = 0; virtual void clearRect(KDContext * ctx, KDRect rect) const = 0; KDSize minimalSizeForOptimalDisplay() const override; void setText(char * textBuffer, size_t textBufferSize); @@ -121,6 +123,7 @@ class TextArea : public TextInput, public InputEventHandler { size_t editedTextLength() const override { return m_text.textLength(); } const Text * getText() const { return &m_text; } bool isAbleToInsertTextAt(int textLength, const char * location, bool shouldRemoveLastCharacter) const override; + void reloadParentheses(const char * text, bool lineBreak); void insertTextAtLocation(const char * text, char * location, int textLength = -1) override; void moveCursorGeo(int deltaX, int deltaY); bool removePreviousGlyph() override; diff --git a/escher/src/text_area.cpp b/escher/src/text_area.cpp index 5427ed5b652..2ac9dd1a5b9 100644 --- a/escher/src/text_area.cpp +++ b/escher/src/text_area.cpp @@ -5,11 +5,13 @@ #include #include + #include #include #include #include + /* TextArea */ TextArea::TextArea(Responder * parentResponder, View * contentView, const KDFont * font) : @@ -514,13 +516,20 @@ void TextArea::ContentView::drawRect(KDContext * ctx, KDRect rect) const { ); int y = 0; - + StaticTable mismatchedParenthesesPositions; + initStaticTable(&mismatchedParenthesesPositions); + findMismatchedParentheses(m_text.text(), &mismatchedParenthesesPositions); + int charBefore = 0; + int bracketBalance = 0; for (Text::Line line : m_text) { KDCoordinate width = line.glyphWidth(m_font); if (y >= topLeft.line() && y <= bottomRight.line() && topLeft.column() < (int)width) { - drawLine(ctx, y, line.text(), line.charLength(), topLeft.column(), bottomRight.column(), m_selectionStart, m_selectionEnd); + drawLine(ctx, y, line.text(), line.charLength(), topLeft.column(), bottomRight.column(), m_selectionStart, m_selectionEnd, &mismatchedParenthesesPositions, charBefore, bracketBalance); } + bracketBalance += getBracketBalance(line.text()); + charBefore += line.charLength() + 1; // The +1 is to count the newline character y++; + } } @@ -537,7 +546,6 @@ void TextArea::ContentView::drawStringAt(KDContext * ctx, int line, int column, KDSize glyphSize = usedFont->glyphSize(); bool drawSelection = selectionStart != nullptr && selectionEnd > text && selectionStart < text + length; - KDPoint nextPoint = ctx->drawString( text, KDPoint(column*glyphSize.width(), line*glyphSize.height()), @@ -597,6 +605,20 @@ bool TextArea::ContentView::isAbleToInsertTextAt(int textLength, const char * lo return m_text.textLength() + textLength - removedCharacters < m_text.bufferSize() && textLength != 0; } +void TextArea::ContentView::reloadParentheses(const char * text, bool lineBreak) { + for (int i = 0; text[i] != '\0'; i++) { + if ( + text[i] == '(' || + text[i] == '[' || + text[i] == '{' || + text[i] == ')' || + text[i] == ']' || + text[i] == '}' + ) { + reloadRectFromPosition(&text[i], lineBreak); + } + } +} void TextArea::ContentView::insertTextAtLocation(const char * text, char * location, int textLength) { int textLen = textLength < 0 ? strlen(text) : textLength; assert(textLen < 0 || textLen <= strlen(text)); @@ -609,6 +631,7 @@ void TextArea::ContentView::insertTextAtLocation(const char * text, char * locat // Replace System parentheses (used to keep layout tree structure) by normal parentheses Poincare::SerializationHelper::ReplaceSystemParenthesesByUserParentheses(location, textLen); reloadRectFromPosition(location, lineBreak); + reloadParentheses(m_text.text(), lineBreak); } bool TextArea::ContentView::removePreviousGlyph() { @@ -622,14 +645,15 @@ bool TextArea::ContentView::removePreviousGlyph() { setCursorLocation(cursorLoc); // Update the cursor layoutSubviews(); // Reposition the cursor reloadRectFromPosition(cursorLocation(), lineBreak); + reloadParentheses(m_text.text(), lineBreak); return true; } bool TextArea::ContentView::removeEndOfLine() { size_t removedLine = m_text.removeRemainingLine(cursorLocation(), 1); + reloadParentheses(m_text.text(), true); if (removedLine > 0) { layoutSubviews(); - reloadRectFromPosition(cursorLocation(), false); return true; } return false; @@ -645,6 +669,7 @@ bool TextArea::ContentView::removeStartOfLine() { assert(cursorLocation() >= text() + removedLine); setCursorLocation(cursorLocation() - removedLine); reloadRectFromPosition(cursorLocation(), true); + reloadParentheses(m_text.text(), true); return true; } return false;