From 8f97a332f610a2e8d7666665274580a7f8fd5fc0 Mon Sep 17 00:00:00 2001 From: Arthur Camouseigt Date: Mon, 29 Jun 2020 15:33:10 +0200 Subject: [PATCH] [Python] Modified the paste effect in script and shell area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a formula is pasted in a script or in the shell, some symbols are replaced by their equivalent in python : x turns into * ^ turns into ** √ turns into sqrt etc Change-Id: If6f2a22d4f3c148c2655e0892023b0e28058a9a6 --- apps/code/app.cpp | 2 + apps/code/helpers.cpp | 40 ++------ escher/Makefile | 1 + escher/include/escher/clipboard.h | 32 ++++++- escher/src/clipboard.cpp | 5 + escher/src/text_field.cpp | 1 + escher/test/clipboard.cpp | 21 +++++ ion/include/ion/unicode/utf8_helper.h | 32 +++++++ ion/src/shared/unicode/utf8_helper.cpp | 123 +++++++++++++++++++------ ion/test/utf8_helper.cpp | 71 +++++++++++++- 10 files changed, 261 insertions(+), 67 deletions(-) create mode 100644 escher/test/clipboard.cpp diff --git a/apps/code/app.cpp b/apps/code/app.cpp index 5ed4e204b..63537618c 100644 --- a/apps/code/app.cpp +++ b/apps/code/app.cpp @@ -85,11 +85,13 @@ App::App(Snapshot * snapshot) : m_codeStackViewController(&m_modalViewController, &m_listFooter), m_variableBoxController(snapshot->scriptStore()) { + Clipboard::sharedClipboard()->enterPython(); } App::~App() { assert(!m_consoleController.inputRunLoopActive()); deinitPython(); + Clipboard::sharedClipboard()->exitPython(); } bool App::handleEvent(Ion::Events::Event event) { diff --git a/apps/code/helpers.cpp b/apps/code/helpers.cpp index 5634dbf67..18250e77f 100644 --- a/apps/code/helpers.cpp +++ b/apps/code/helpers.cpp @@ -1,44 +1,20 @@ #include "helpers.h" -#include -#include -#include +#include namespace Code { namespace Helpers { -class EventTextPair { -public: - constexpr EventTextPair(Ion::Events::Event event, const char * text) : m_event(event), m_text(text) {} - Ion::Events::Event event() const { return m_event; } - const char * text() const { return m_text; } -private: - const Ion::Events::Event m_event; - const char * m_text; -}; - -static_assert('\x11' == UCodePointEmpty, "Unicode error"); -static constexpr EventTextPair sEventTextMap[] = { - EventTextPair(Ion::Events::XNT, "x"), - EventTextPair(Ion::Events::Exp, "exp(\x11)"), - EventTextPair(Ion::Events::Ln, "log(\x11)"), - EventTextPair(Ion::Events::Log, "log10(\x11)"), - EventTextPair(Ion::Events::Imaginary, "1j"), - EventTextPair(Ion::Events::Power, "**"), - EventTextPair(Ion::Events::Pi, "pi"), - EventTextPair(Ion::Events::Sqrt, "sqrt(\x11)"), - EventTextPair(Ion::Events::Square, "**2"), - EventTextPair(Ion::Events::Multiplication, "*"), - EventTextPair(Ion::Events::EE, "e"), -}; - const char * PythonTextForEvent(Ion::Events::Event event) { - for (size_t i=0; i #include +#include class Clipboard { public: static Clipboard * sharedClipboard(); void store(const char * storedText, int length = -1); - const char * storedText() const { return m_textBuffer; } + const char * storedText() { return m_textBuffer; } void reset(); + void enterPython() { replaceCharForPython(true); } + void exitPython() { replaceCharForPython(false); } + static constexpr int k_bufferSize = TextField::maxBufferSize(); private: - char m_textBuffer[TextField::maxBufferSize()]; + char m_textBuffer[k_bufferSize]; + void replaceCharForPython(bool entersPythonApp); +}; + + +/* The order in which the text pairs are stored is important. Indeed when leaving + * python, the text stored in the buffer is converted into an input for other + * apps. Therefore if we want to convert "3**3" into "3^3", the function must + * look for "**" paterns before "*". Otherwise, we will get "3××3". */ +static constexpr int NumberOfPythonTextPairs = 12; +static constexpr UTF8Helper::TextPair PythonTextPairs[NumberOfPythonTextPairs] = { + UTF8Helper::TextPair("√(\x11)", "sqrt(\x11)", true), + UTF8Helper::TextPair("ℯ^(\x11)", "exp(\x11)", true), + UTF8Helper::TextPair("log(\x11)", "log10(\x11)", true), + UTF8Helper::TextPair("ln(\x11)", "log(\x11)", true), + UTF8Helper::TextPair("ᴇ", "e"), + UTF8Helper::TextPair("𝐢", "1j"), + /* Since textPairs are also used to pair events, we need to keep both ^2 and ^ + * to get the desired behavior in python when using power or square key*/ + UTF8Helper::TextPair("^2", "**2"), + UTF8Helper::TextPair("^", "**"), + UTF8Helper::TextPair("π", "pi"), + UTF8Helper::TextPair("×", "*"), + UTF8Helper::TextPair("·", "*"), + UTF8Helper::TextPair("][", "], ["), }; #endif diff --git a/escher/src/clipboard.cpp b/escher/src/clipboard.cpp index a8551bc2b..5fa300799 100644 --- a/escher/src/clipboard.cpp +++ b/escher/src/clipboard.cpp @@ -1,4 +1,5 @@ #include +#include #include static Clipboard s_clipboard; @@ -14,3 +15,7 @@ void Clipboard::store(const char * storedText, int length) { void Clipboard::reset() { strlcpy(m_textBuffer, "", 1); } + +void Clipboard::replaceCharForPython(bool entersPythonApp) { + UTF8Helper::tryAndReplacePatternsInStringByPatterns((char *)m_textBuffer, TextField::maxBufferSize(), (UTF8Helper::TextPair *)&PythonTextPairs, NumberOfPythonTextPairs, entersPythonApp); +} \ No newline at end of file diff --git a/escher/src/text_field.cpp b/escher/src/text_field.cpp index b16e5c70f..a7101b9b5 100644 --- a/escher/src/text_field.cpp +++ b/escher/src/text_field.cpp @@ -123,6 +123,7 @@ bool TextField::ContentView::insertTextAtLocation(const char * text, char * loca assert(m_isEditing); size_t textLength = textLen < 0 ? strlen(text) : (size_t)textLen; + // TODO when paste fails because of a too big message, create a pop-up if (m_currentDraftTextLength + textLength >= m_draftTextBufferSize || textLength == 0) { return false; } diff --git a/escher/test/clipboard.cpp b/escher/test/clipboard.cpp new file mode 100644 index 000000000..7c895c9b4 --- /dev/null +++ b/escher/test/clipboard.cpp @@ -0,0 +1,21 @@ +#include +#include +#include + +void assert_clipboard_enters_and_exits_python(const char * string, const char * stringResult) { + Clipboard * clipboard = Clipboard::sharedClipboard(); + clipboard->store(string); + clipboard->enterPython(); + quiz_assert(strcmp(clipboard->storedText(), stringResult) == 0); + clipboard->exitPython(); + quiz_assert(strcmp(clipboard->storedText(), string) == 0); +} + +QUIZ_CASE(escher_clipboard_enters_and_exits_python) { + assert_clipboard_enters_and_exits_python("4×4", "4*4"); + assert_clipboard_enters_and_exits_python("ℯ^(ln(4))", "exp(log(4))"); + assert_clipboard_enters_and_exits_python("ln(log(ln(π)))^𝐢", "log(log10(log(pi)))**1j"); + assert_clipboard_enters_and_exits_python("√(1ᴇ10)", "sqrt(1e10)"); + assert_clipboard_enters_and_exits_python("1×𝐢^2", "1*1j**2"); + assert_clipboard_enters_and_exits_python("12^(1/4)×(π/6)×(12×π)^(1/4)", "12**(1/4)*(pi/6)*(12*pi)**(1/4)"); +} diff --git a/ion/include/ion/unicode/utf8_helper.h b/ion/include/ion/unicode/utf8_helper.h index 96404b863..536086422 100644 --- a/ion/include/ion/unicode/utf8_helper.h +++ b/ion/include/ion/unicode/utf8_helper.h @@ -6,6 +6,19 @@ namespace UTF8Helper { +class TextPair { +public: + constexpr TextPair(const char * firstString, const char * secondString, bool removeParenthesesExtention = false) : m_firstString(firstString), m_secondString(secondString), m_removeParenthesesExtention(removeParenthesesExtention){} + const char * firstString() { return m_firstString; } + const char * secondString() { return m_secondString; } + bool removeParenthesesExtention() { return m_removeParenthesesExtention; } + static constexpr int k_maxLength = 20; +private: + const char * m_firstString; + const char * m_secondString; + bool m_removeParenthesesExtention; +}; + // Returns the number of occurences of a code point in a string int CountOccurrences(const char * s, CodePoint c); @@ -28,6 +41,25 @@ bool CopyAndRemoveCodePoints(char * dst, size_t dstSize, const char * src, CodeP * points where removed before it. Ensure null-termination of dst. */ void RemoveCodePoint(char * buffer, CodePoint c, const char * * indexToUpdate = nullptr, const char * stoppingPosition = nullptr); +/* Slides a string by a number of chars. If slidingSize < 0, the string is slided + * to the left losing the first chars. Returns true if successful. + * Exemples : + * slideStringByNumberOfChar("12345", 2, 7) gives "1212345" + * slideStringByNumberOfChar("12345", 2, 5) gives "12123" + * slideStringByNumberOfChar("12345", -2, 5) gives "34545"*/ +bool slideStringByNumberOfChar(char * text, int slidingSize, int textMaxLength); + +/* Looks for patterns in a string. If a pattern is found, it is replaced by + * the one associated in the TextPair struct. + * - firstToSecond defines if replace the first string of a TextPair by the second + * or the other way around. + * - indexToUpdate is a pointer to a char in the string. It will be updated to + * point to the same place after calling the function. + * - stoppingPosition allows partial replacement in the string. + * + * Ensure null termination of the string or set the value of stoppingPosition*/ +void tryAndReplacePatternsInStringByPatterns(char * text, int textMaxSize, TextPair * textPairs, int numberOfPairs, bool firstToSecond, const char * * indexToUpdate = nullptr, const char * stoppingPosition = nullptr); + /* Copy src into dst until end of dst or code point c, with null termination. Return the length of the copy */ size_t CopyUntilCodePoint(char * dst, size_t dstSize, const char * src, CodePoint c); diff --git a/ion/src/shared/unicode/utf8_helper.cpp b/ion/src/shared/unicode/utf8_helper.cpp index e3ae93fba..71b2f9a09 100644 --- a/ion/src/shared/unicode/utf8_helper.cpp +++ b/ion/src/shared/unicode/utf8_helper.cpp @@ -132,37 +132,104 @@ bool CopyAndRemoveCodePoints(char * dst, size_t dstSize, const char * src, CodeP } void RemoveCodePoint(char * buffer, CodePoint c, const char * * pointerToUpdate, const char * stoppingPosition) { - UTF8Decoder decoder(buffer); - const char * currentPointer = buffer; - CodePoint codePoint = decoder.nextCodePoint(); - const char * initialPointerToUpdate = *pointerToUpdate; - const char * nextPointer = decoder.stringPosition(); - size_t bufferIndex = 0; + constexpr int patternMaxSize = CodePoint::MaxCodePointCharLength + 1; // +1 for null terminating char + char pattern[patternMaxSize]; int codePointCharSize = UTF8Decoder::CharSizeOfCodePoint(c); - (void)codePointCharSize; // Silence compilation warning about unused variable. + UTF8Decoder::CodePointToChars(c, pattern, codePointCharSize); + pattern[codePointCharSize] = '\0'; + TextPair pair(pattern, ""); + tryAndReplacePatternsInStringByPatterns(buffer, strlen(buffer), &pair, 1, true, pointerToUpdate, stoppingPosition); +} - while (codePoint != UCodePointNull && (stoppingPosition == nullptr || currentPointer < stoppingPosition)) { - if (codePoint != c) { - int copySize = nextPointer - currentPointer; - memmove(buffer + bufferIndex, currentPointer, copySize); - bufferIndex+= copySize; - } else if (pointerToUpdate != nullptr && currentPointer < initialPointerToUpdate) { - assert(*pointerToUpdate - buffer >= codePointCharSize); - *pointerToUpdate = *pointerToUpdate - codePointCharSize; - } - currentPointer = nextPointer; - codePoint = decoder.nextCodePoint(); - nextPointer = decoder.stringPosition(); +bool slideStringByNumberOfChar(char * text, int slidingSize, int textMaxLength) { + int lenText = strlen(text); + if (lenText + slidingSize > textMaxLength || lenText + slidingSize < 0) { + return false; } - if (codePoint == UCodePointNull) { - *(buffer + bufferIndex) = 0; - } else { - assert(stoppingPosition != nullptr); - // Find the null-terminating code point - const char * nullTermination = currentPointer + strlen(currentPointer); - /* Copy what remains of the buffer after the stopping position for code - * point removal */ - memmove(buffer + bufferIndex, stoppingPosition, nullTermination - stoppingPosition + 1); + if (slidingSize > 0) { + memmove(text+slidingSize, text, strlen(text)+1); + } else if (slidingSize < 0) { + memmove(text, text-slidingSize, strlen(text)+1); + } + // In case slidingSize = 0, there is nothing to do + return true; +} + +/* Replaces the first chars of a string by other ones. If the sizes are different + * the rest of the string will be moved right after the replacement chars. + * If successful returns true.*/ +static bool replaceFirstCharsByPattern(char * text, int lengthOfPatternToRemove, const char * replacementPattern, int textMaxLength) { + int lengthOfReplacementPattern = strlen(replacementPattern); + if (lengthOfPatternToRemove <= strlen(text) && slideStringByNumberOfChar(text, lengthOfReplacementPattern-lengthOfPatternToRemove, textMaxLength)) { + for (int i = 0; i < lengthOfReplacementPattern; i++) { + text[i] = replacementPattern[i]; + } + return true; + } + return false; +} + +void tryAndReplacePatternsInStringByPatterns(char * text, int textMaxLength, TextPair * textPairs, int numberOfPairs, bool firstToSecond, const char * * pointerToUpdate, const char * stoppingPosition) { + size_t i = 0; + size_t iPrev = 0; + size_t textLength = strlen(text); + size_t lengthOfParenthesisExtention = strlen("(\x11)"); + while(i < textLength) { + iPrev = i; + bool didReplace = false; + for (int j = 0; j < numberOfPairs; j++) { + TextPair p = textPairs[j]; + size_t firstStringLength = strlen(p.firstString()); + size_t secondStringLength = strlen(p.secondString()); + /* Instead of storing TextPair("√(\x11)", "sqrt(\x11)") for the keyboard + * events and TextPair("√", "sqrt") for the copy paste, we store just the + * first and register it as "function". Therefore we can decide to remove + * the (\x11) part or not depending on the application. This process is + * repeated for all 4 function keys usable in python (√, ℯ, ln, log)*/ + if (p.removeParenthesesExtention()) { + firstStringLength -= lengthOfParenthesisExtention; + secondStringLength -= lengthOfParenthesisExtention; + } + char firstString[TextPair::k_maxLength]; + char secondString[TextPair::k_maxLength]; + // Getting rid of the eventual (\x11) part + strlcpy((char *)firstString, p.firstString(), firstStringLength+1); + strlcpy((char *)secondString, p.secondString(), secondStringLength+1); + + char * matchedString = firstToSecond ? firstString : secondString; + size_t matchedStringLength = strlen(matchedString); + char * replacingString = firstToSecond ? secondString : firstString; + size_t replacingStringLength = strlen(replacingString); + + if (strncmp(&text[i], matchedString, matchedStringLength) == 0) { + didReplace = replaceFirstCharsByPattern(&text[i], matchedStringLength, replacingString, textMaxLength); + if (didReplace) { + int delta = replacingStringLength - matchedStringLength; + textLength += delta; + if (pointerToUpdate != nullptr && &text[i] < *pointerToUpdate) { + // We still have to update the pointer as the modification cursor has not yet exceeded it. + *pointerToUpdate = *pointerToUpdate + delta; + } + if (stoppingPosition != nullptr) { + stoppingPosition = stoppingPosition + delta; + } + if (replacingStringLength != 0) { + i += replacingStringLength - 1; + /* When working with multiple TextPairs at the same time, it can be + * usefull to go back by one char. That is the case for empty matrixes + * Indeed, in the string ",,]", ",," is replaced by ",\x11,". + * The ",]" pattern right after would be missed if not for the -1.*/ + } + } + } + } + if (iPrev == i && !didReplace) { + // In case no pattern matched with the text, we go to the next char. + i++; + } + if ((stoppingPosition != nullptr) && (&text[i] >= stoppingPosition)) { + break; + } } } diff --git a/ion/test/utf8_helper.cpp b/ion/test/utf8_helper.cpp index b96f361d9..4434bf27e 100644 --- a/ion/test/utf8_helper.cpp +++ b/ion/test/utf8_helper.cpp @@ -53,9 +53,8 @@ void assert_copy_and_remove_code_points_gives(char * dst, size_t dstSize, const quiz_assert(dst[i] == result[i]); } } - +static int bufferSize = 100; QUIZ_CASE(ion_utf8_copy_and_remove_code_point) { - constexpr int bufferSize = 100; char buffer[bufferSize]; const char * s = "12345"; @@ -116,7 +115,6 @@ void assert_remove_code_point_gives(char * buffer, CodePoint c, const char * * i } QUIZ_CASE(ion_utf8_remove_code_point) { - constexpr int bufferSize = 100; char buffer[bufferSize]; const char * s = "2345"; @@ -165,13 +163,77 @@ QUIZ_CASE(ion_utf8_remove_code_point) { assert_remove_code_point_gives(buffer, c, &indexToUpdate, stoppingPosition, indexToUpdateResult, result); } +void assert_slide_string_by_number_of_char_gives(const char * string, int slidingSize, bool successResult, const char * stringResult = nullptr) { + char buffer[bufferSize]; + strlcpy(buffer, string, bufferSize); + bool success = UTF8Helper::slideStringByNumberOfChar((char *)buffer, slidingSize, bufferSize); + quiz_assert(success == successResult); + if (successResult) { + quiz_assert(strncmp(buffer, stringResult, bufferSize) == 0); + } +} + + +QUIZ_CASE(ion_utf8_move_string_from_index_by_number_of_char) { + const char * string1 = "12345"; + assert_slide_string_by_number_of_char_gives(string1, 1, true, "112345"); + const char * string2 = "(1+3)"; + assert_slide_string_by_number_of_char_gives(string2, 3, true, "(1+(1+3)"); + assert_slide_string_by_number_of_char_gives(string2, bufferSize - strlen(string2)/2, false); + const char * string3 = "exp(3+4)"; + assert_slide_string_by_number_of_char_gives(string3, -3, true, "(3+4)"); + assert_slide_string_by_number_of_char_gives(string3, -(strlen(string3)+3), false); + assert_slide_string_by_number_of_char_gives(string3, -8, true, ""); +} + +void assert_try_and_replace_pattern_in_string_by_pattern_gives(char * buffer, int bufferSize, UTF8Helper::TextPair * textPairs, int numberOfPairs, bool firstToSecond, const char * stringResult, const char ** indexToUpdate = nullptr, const char * indexToUpdateResult = nullptr, const char * stoppingPosition = nullptr) { + UTF8Helper::tryAndReplacePatternsInStringByPatterns(buffer, bufferSize, textPairs, numberOfPairs, firstToSecond, indexToUpdate, stoppingPosition); + quiz_assert(strncmp(buffer, stringResult, bufferSize) == 0); + if (indexToUpdateResult != nullptr) { + quiz_assert(*indexToUpdate == indexToUpdateResult); + } +} + +QUIZ_CASE(ion_utf8_try_and_replace_pattern_in_string_by_pattern) { + constexpr int numberOfPairs = 2; + constexpr UTF8Helper::TextPair textPairs[numberOfPairs] = { + UTF8Helper::TextPair("12", "2.3"), + UTF8Helper::TextPair("exp", "ln"), + }; + + char buffer[bufferSize]; + const char * string = "1234512"; + strlcpy(buffer, string, bufferSize); + const char * indexToUpdate = buffer + 3; + const char * indexToUpdateResult = indexToUpdate + 1; + const char * result = "2.33452.3"; + const char * stoppingPosition = nullptr; + assert_try_and_replace_pattern_in_string_by_pattern_gives(buffer, bufferSize, (UTF8Helper::TextPair *)&textPairs, numberOfPairs, true, result, &indexToUpdate, indexToUpdateResult); + + string = "exp(2.3)12"; + strlcpy(buffer, string, bufferSize); + indexToUpdate = buffer + 3; + indexToUpdateResult = indexToUpdate - 1; + result = "ln(2.3)12"; + stoppingPosition = buffer + 5; + assert_try_and_replace_pattern_in_string_by_pattern_gives(buffer, bufferSize, (UTF8Helper::TextPair *)&textPairs, numberOfPairs, true, result, &indexToUpdate, indexToUpdateResult, stoppingPosition); + + string = "12*ln(7)+ln"; + strlcpy(buffer, string, bufferSize); + indexToUpdate = buffer + 7; + indexToUpdateResult = indexToUpdate + 1; + result = "12*exp(7)+ln"; + stoppingPosition = buffer + 7; + assert_try_and_replace_pattern_in_string_by_pattern_gives(buffer, bufferSize, (UTF8Helper::TextPair *)&textPairs, numberOfPairs, false, result, &indexToUpdate, indexToUpdateResult, stoppingPosition); + +} + void assert_string_copy_until_code_point_gives(char * dst, size_t dstSize, const char * src, CodePoint c, const char * result, size_t returnedResult) { quiz_assert(UTF8Helper::CopyUntilCodePoint(dst, dstSize, src, c) == returnedResult); quiz_assert(strcmp(dst, result) == 0); } QUIZ_CASE(ion_utf8_helper_copy_until_code_point) { - constexpr int bufferSize = 100; char buffer[bufferSize]; const char * s = "1234"; @@ -217,7 +279,6 @@ void assert_string_remove_previous_glyph_gives(const char * text, char * locatio } QUIZ_CASE(ion_utf8_helper_remove_previous_glyph) { - constexpr int bufferSize = 100; char buffer[bufferSize]; // 3é4 buffer[0] = '3';