From 87c87b51fce07de580958c238cf92e8e1bc12de6 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 31 May 2024 06:17:16 -0500 Subject: [PATCH] Add support for regex search to conhost and Terminal (#17316) This is broken down into individual reviewable commits. [Here is](https://github.com/microsoft/terminal/assets/189190/3b2ffd50-1350-4f3c-86b0-75abbd846969) a video of it in action! Part of #3920 (cherry picked from commit ecb56314762dcb9b41e177d05d669b28bbbe1490) Service-Card-Id: PVTI_lADOAF3p4s4AmhmszgTTM0g Service-Version: 1.21 --- .github/actions/spelling/expect/expect.txt | 1 + src/buffer/out/search.cpp | 17 ++++-- src/buffer/out/search.h | 18 +++++- src/buffer/out/textBuffer.cpp | 28 +++++++-- src/buffer/out/textBuffer.hpp | 5 +- .../out/ut_textbuffer/UTextAdapterTests.cpp | 7 ++- src/cascadia/TerminalControl/ControlCore.cpp | 10 ++- src/cascadia/TerminalControl/ControlCore.h | 2 +- src/cascadia/TerminalControl/ControlCore.idl | 3 +- .../Resources/en-US/Resources.resw | 14 ++++- .../TerminalControl/SearchBoxControl.cpp | 38 +++++++++--- .../TerminalControl/SearchBoxControl.h | 4 +- .../TerminalControl/SearchBoxControl.idl | 3 +- .../TerminalControl/SearchBoxControl.xaml | 11 ++++ src/cascadia/TerminalControl/TermControl.cpp | 17 +++--- src/cascadia/TerminalControl/TermControl.h | 4 +- src/cascadia/TerminalCore/Terminal.cpp | 2 +- src/host/res.rc | 1 + src/host/resource.h | 1 + src/host/selectionInput.cpp | 2 +- src/host/ut_host/SearchTests.cpp | 61 ++++++++++++++++--- src/interactivity/win32/find.cpp | 12 ++-- src/interactivity/win32/resource.h | 1 + src/types/UiaTextRangeBase.cpp | 4 +- 24 files changed, 203 insertions(+), 63 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 5c0c14b0df9..c2924b2ab75 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -607,6 +607,7 @@ FILTERONPASTE FINDCASE FINDDLG FINDDOWN +FINDREGEX FINDSTRINGEXACT FINDUP FIter diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 3fe7ca0c8c1..31c7081b2b9 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,24 +8,26 @@ using namespace Microsoft::Console::Types; -bool Search::IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept +bool Search::IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags) const noexcept { return _renderData != &renderData || _needle != needle || - _caseInsensitive != caseInsensitive || + _flags != flags || _lastMutationId != renderData.GetTextBuffer().GetLastMutationId(); } -bool Search::Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse) +bool Search::Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags, bool reverse) { const auto& textBuffer = renderData.GetTextBuffer(); _renderData = &renderData; _needle = needle; - _caseInsensitive = caseInsensitive; + _flags = flags; _lastMutationId = textBuffer.GetLastMutationId(); - _results = textBuffer.SearchText(needle, caseInsensitive); + auto result = textBuffer.SearchText(needle, _flags); + _ok = result.has_value(); + _results = std::move(result).value_or(std::vector{}); _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; _step = reverse ? -1 : 1; return true; @@ -144,3 +146,8 @@ ptrdiff_t Search::CurrentMatch() const noexcept { return _index; } + +bool Search::IsOk() const noexcept +{ + return _ok; +} diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index c991eb92e7f..763b5d40c73 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -20,13 +20,23 @@ Revision History: #include "textBuffer.hpp" #include "../renderer/inc/IRenderData.hpp" +enum class SearchFlag : unsigned int +{ + None = 0, + + CaseInsensitive = 1 << 0, + RegularExpression = 1 << 1, +}; + +DEFINE_ENUM_FLAG_OPERATORS(SearchFlag); + class Search final { public: Search() = default; - bool IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept; - bool Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse); + bool IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags) const noexcept; + bool Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags, bool reverse); void MoveToCurrentSelection(); void MoveToPoint(til::point anchor) noexcept; @@ -39,14 +49,16 @@ class Search final const std::vector& Results() const noexcept; std::vector&& ExtractResults() noexcept; ptrdiff_t CurrentMatch() const noexcept; + bool IsOk() const noexcept; private: // _renderData is a pointer so that Search() is constexpr default constructable. Microsoft::Console::Render::IRenderData* _renderData = nullptr; std::wstring _needle; - bool _caseInsensitive = false; + SearchFlag _flags{}; uint64_t _lastMutationId = 0; + bool _ok{ false }; std::vector _results; ptrdiff_t _index = 0; ptrdiff_t _step = 0; diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index ee3dc3a5088..10d85e6b262 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -12,6 +12,7 @@ #include "../../types/inc/GlyphWidth.hpp" #include "../renderer/base/renderer.hpp" #include "../types/inc/utils.hpp" +#include "search.h" using namespace Microsoft::Console; using namespace Microsoft::Console::Types; @@ -3193,14 +3194,15 @@ void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other) // Searches through the entire (committed) text buffer for `needle` and returns the coordinates in absolute coordinates. // The end coordinates of the returned ranges are considered inclusive. -std::vector TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive) const +std::optional> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags) const { - return SearchText(needle, caseInsensitive, 0, til::CoordTypeMax); + return SearchText(needle, flags, 0, til::CoordTypeMax); } // Searches through the given rows [rowBeg,rowEnd) for `needle` and returns the coordinates in absolute coordinates. // While the end coordinates of the returned ranges are considered inclusive, the [rowBeg,rowEnd) range is half-open. -std::vector TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const +// Returns nullopt if the parameters were invalid (e.g. regex search was requested with an invalid regex) +std::optional> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const { rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1); @@ -3214,11 +3216,25 @@ std::vector TextBuffer::SearchText(const std::wstring_view& nee auto text = ICU::UTextFromTextBuffer(*this, rowBeg, rowEnd); - uint32_t flags = UREGEX_LITERAL; - WI_SetFlagIf(flags, UREGEX_CASE_INSENSITIVE, caseInsensitive); + uint32_t icuFlags{ 0 }; + WI_SetFlagIf(icuFlags, UREGEX_CASE_INSENSITIVE, WI_IsFlagSet(flags, SearchFlag::CaseInsensitive)); + + if (WI_IsFlagSet(flags, SearchFlag::RegularExpression)) + { + WI_SetFlag(icuFlags, UREGEX_MULTILINE); + } + else + { + WI_SetFlag(icuFlags, UREGEX_LITERAL); + } UErrorCode status = U_ZERO_ERROR; - const auto re = ICU::CreateRegex(needle, flags, &status); + const auto re = ICU::CreateRegex(needle, icuFlags, &status); + if (status > U_ZERO_ERROR) + { + return std::nullopt; + } + uregex_setUText(re.get(), &text, &status); if (uregex_find(re.get(), -1, &status)) diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 5a8a5c16547..6f21ad41cb4 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -58,6 +58,7 @@ filling in the last row, and updating the screen. #include "../buffer/out/textBufferTextIterator.hpp" struct URegularExpression; +enum class SearchFlag : unsigned int; namespace Microsoft::Console::Render { @@ -293,8 +294,8 @@ class TextBuffer final static void Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Microsoft::Console::Types::Viewport* lastCharacterViewport = nullptr, PositionInformation* positionInfo = nullptr); - std::vector SearchText(const std::wstring_view& needle, bool caseInsensitive) const; - std::vector SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const; + std::optional> SearchText(const std::wstring_view& needle, SearchFlag flags) const; + std::optional> SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const; // Mark handling std::vector GetMarkRows() const; diff --git a/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp b/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp index 427e90e6dac..24d04a56ff8 100644 --- a/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp +++ b/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp @@ -6,6 +6,7 @@ #include "WexTestClass.h" #include "../textBuffer.hpp" #include "../../renderer/inc/DummyRenderer.hpp" +#include "../search.h" template<> class WEX::TestExecution::VerifyOutputTraits> @@ -49,15 +50,15 @@ class UTextAdapterTests }; auto expected = std::vector{ s(0, 2), s(8, 10) }; - auto actual = buffer.SearchText(L"abc", false); + auto actual = buffer.SearchText(L"abc", SearchFlag::None); VERIFY_ARE_EQUAL(expected, actual); expected = std::vector{ s(5, 5) }; - actual = buffer.SearchText(L"𝒷", false); + actual = buffer.SearchText(L"𝒷", SearchFlag::None); VERIFY_ARE_EQUAL(expected, actual); expected = std::vector{ s(12, 15) }; - actual = buffer.SearchText(L"ネコ", false); + actual = buffer.SearchText(L"ネコ", SearchFlag::None); VERIFY_ARE_EQUAL(expected, actual); } }; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 3ace9a25f7c..1fc430575e8 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1654,10 +1654,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - resetOnly: If true, only Reset() will be called, if anything. FindNext() will never be called. // Return Value: // - - SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool resetOnly) + SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool regularExpression, const bool resetOnly) { const auto lock = _terminal->LockForWriting(); - const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), text, !caseSensitive); + SearchFlag flags{}; + WI_SetFlagIf(flags, SearchFlag::CaseInsensitive, !caseSensitive); + WI_SetFlagIf(flags, SearchFlag::RegularExpression, regularExpression); + const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), text, flags); if (searchInvalidated || !resetOnly) { @@ -1666,7 +1669,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (searchInvalidated) { oldResults = _searcher.ExtractResults(); - _searcher.Reset(*_terminal.get(), text, !caseSensitive, !goForward); + _searcher.Reset(*_terminal.get(), text, flags, !goForward); if (SnapSearchResultToSelection()) { @@ -1700,6 +1703,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation .TotalMatches = totalMatches, .CurrentMatch = currentMatch, .SearchInvalidated = searchInvalidated, + .SearchRegexInvalid = !_searcher.IsOk(), }; } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 1f0232877af..f12d76bcc0b 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -219,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetSelectionAnchor(const til::point position); void SetEndSelectionPoint(const til::point position); - SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool reset); + SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool regularExpression, bool reset); const std::vector& SearchResultRows() const noexcept; void ClearSearch(); void SnapSearchResultToSelection(bool snap) noexcept; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index d10a963786e..4ded4e2ed89 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -54,6 +54,7 @@ namespace Microsoft.Terminal.Control Int32 TotalMatches; Int32 CurrentMatch; Boolean SearchInvalidated; + Boolean SearchRegexInvalid; }; [default_interface] runtimeclass SelectionColor @@ -134,7 +135,7 @@ namespace Microsoft.Terminal.Control void ResumeRendering(); void BlinkAttributeTick(); - SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean reset); + SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean regularExpression, Boolean reset); void ClearSearch(); Boolean SnapSearchResultToSelection; diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index 676e3e327ce..2d0efed81fc 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -304,4 +304,16 @@ Please either install the missing font or choose another one. Restored "Restored" as in "This content was restored" - \ No newline at end of file + + Regular Expression + The tooltip text for the button on the search box control governing the use of "regular expressions" ("regex"). + + + Regular Expression Search + The accessibility description text for the button on the search box control governing the use of "regular expressions" ("regex"). + + + invalid + This brief message is displayed when a regular expression is invalid. + + diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index 0bed679b55a..0a63da28c0e 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -30,6 +30,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); _focusableElements.insert(CaseSensitivityButton()); + _focusableElements.insert(RegexButton()); _focusableElements.insert(GoForwardButton()); _focusableElements.insert(GoBackwardButton()); @@ -235,6 +236,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return CaseSensitivityButton().IsChecked().GetBoolean(); } + bool SearchBoxControl::RegularExpression() + { + return RegexButton().IsChecked().GetBoolean(); + } + // Method Description: // - Handler for pressing Enter on TextBox, trigger // text search @@ -256,11 +262,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) { - Search.raise(Text(), !GoForward(), CaseSensitive()); + Search.raise(Text(), !GoForward(), CaseSensitive(), RegularExpression()); } else { - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } e.Handled(true); } @@ -351,7 +357,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -372,7 +378,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -410,7 +416,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(Text(), GoForward(), CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -422,7 +428,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(Text(), GoForward(), CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); + } + + void SearchBoxControl::RegexButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -515,7 +526,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation double SearchBoxControl::_GetStatusMaxWidth() { const auto fontSize = StatusBox().FontSize(); - const auto maxLength = std::max({ _TextWidth(_FormatStatus(-1, -1), fontSize), + const auto maxLength = std::max({ _TextWidth(RS_(L"SearchRegexInvalid"), fontSize), + _TextWidth(_FormatStatus(-1, -1), fontSize), _TextWidth(_FormatStatus(0, -1), fontSize), _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus, MaximumTotalResultsToShowInStatus - 1), fontSize), _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus - 1), fontSize), @@ -532,9 +544,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - currentMatch - the index of the current match (0-based) // Return Value: // - - void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch) + void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid) { - const auto status = _FormatStatus(totalMatches, currentMatch); + hstring status; + if (searchRegexInvalid) + { + status = RS_(L"SearchRegexInvalid"); + } + else + { + status = _FormatStatus(totalMatches, currentMatch); + } StatusBox().Text(status); } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 15acd069232..ab6af68c4eb 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -39,10 +39,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::hstring Text(); bool GoForward(); bool CaseSensitive(); + bool RegularExpression(); void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); - void SetStatus(int32_t totalMatches, int32_t currentMatch); + void SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid); void ClearStatus(); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); @@ -51,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void RegexButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void SearchBoxPointerPressedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void SearchBoxPointerReleasedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index 3abf4f50229..61c133b1cce 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -3,7 +3,7 @@ namespace Microsoft.Terminal.Control { - delegate void SearchHandler(String query, Boolean goForward, Boolean isCaseSensitive); + delegate void SearchHandler(String query, Boolean goForward, Boolean isCaseSensitive, Boolean regularExpression); [default_interface] runtimeclass SearchBoxControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged { @@ -11,7 +11,6 @@ namespace Microsoft.Terminal.Control void SetFocusOnTextbox(); void PopulateTextbox(String text); Boolean ContainsFocus(); - void SetStatus(Int32 totalMatches, Int32 currentMatch); void ClearStatus(); Windows.Foundation.Rect ContentClipRect{ get; }; Double OpenAnimationStartPoint{ get; }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index c17fcc3c010..7523cdcfc83 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -243,6 +243,17 @@ + + + +