Skip to content

Commit

Permalink
Add support for regex search to conhost and Terminal (#17316)
Browse files Browse the repository at this point in the history
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 ecb5631)
Service-Card-Id: PVTI_lADOAF3p4s4AmhmszgTTM0g
Service-Version: 1.21
  • Loading branch information
DHowett committed Sep 24, 2024
1 parent dc8a830 commit 87c87b5
Show file tree
Hide file tree
Showing 24 changed files with 203 additions and 63 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ FILTERONPASTE
FINDCASE
FINDDLG
FINDDOWN
FINDREGEX
FINDSTRINGEXACT
FINDUP
FIter
Expand Down
17 changes: 12 additions & 5 deletions src/buffer/out/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<til::point_span>{});
_index = reverse ? gsl::narrow_cast<ptrdiff_t>(_results.size()) - 1 : 0;
_step = reverse ? -1 : 1;
return true;
Expand Down Expand Up @@ -144,3 +146,8 @@ ptrdiff_t Search::CurrentMatch() const noexcept
{
return _index;
}

bool Search::IsOk() const noexcept
{
return _ok;
}
18 changes: 15 additions & 3 deletions src/buffer/out/search.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,14 +49,16 @@ class Search final
const std::vector<til::point_span>& Results() const noexcept;
std::vector<til::point_span>&& 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<til::point_span> _results;
ptrdiff_t _index = 0;
ptrdiff_t _step = 0;
Expand Down
28 changes: 22 additions & 6 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<til::point_span> TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive) const
std::optional<std::vector<til::point_span>> 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<til::point_span> 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<std::vector<til::point_span>> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const
{
rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1);

Expand All @@ -3214,11 +3216,25 @@ std::vector<til::point_span> 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))
Expand Down
5 changes: 3 additions & 2 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<til::point_span> SearchText(const std::wstring_view& needle, bool caseInsensitive) const;
std::vector<til::point_span> SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const;
std::optional<std::vector<til::point_span>> SearchText(const std::wstring_view& needle, SearchFlag flags) const;
std::optional<std::vector<til::point_span>> SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const;

// Mark handling
std::vector<ScrollMark> GetMarkRows() const;
Expand Down
7 changes: 4 additions & 3 deletions src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "WexTestClass.h"
#include "../textBuffer.hpp"
#include "../../renderer/inc/DummyRenderer.hpp"
#include "../search.h"

template<>
class WEX::TestExecution::VerifyOutputTraits<std::vector<til::point_span>>
Expand Down Expand Up @@ -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);
}
};
10 changes: 7 additions & 3 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
// - <none>
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)
{
Expand All @@ -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())
{
Expand Down Expand Up @@ -1700,6 +1703,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
.TotalMatches = totalMatches,
.CurrentMatch = currentMatch,
.SearchInvalidated = searchInvalidated,
.SearchRegexInvalid = !_searcher.IsOk(),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<til::point_span>& SearchResultRows() const noexcept;
void ClearSearch();
void SnapSearchResultToSelection(bool snap) noexcept;
Expand Down
3 changes: 2 additions & 1 deletion src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ namespace Microsoft.Terminal.Control
Int32 TotalMatches;
Int32 CurrentMatch;
Boolean SearchInvalidated;
Boolean SearchRegexInvalid;
};

[default_interface] runtimeclass SelectionColor
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 13 additions & 1 deletion src/cascadia/TerminalControl/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,16 @@ Please either install the missing font or choose another one.</value>
<value>Restored</value>
<comment>"Restored" as in "This content was restored"</comment>
</data>
</root>
<data name="SearchBox_RegularExpression.ToolTipService.ToolTip" xml:space="preserve">
<value>Regular Expression</value>
<comment>The tooltip text for the button on the search box control governing the use of "regular expressions" ("regex").</comment>
</data>
<data name="SearchBox_RegularExpression.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Regular Expression Search</value>
<comment>The accessibility description text for the button on the search box control governing the use of "regular expressions" ("regex").</comment>
</data>
<data name="SearchRegexInvalid" xml:space="preserve">
<value>invalid</value>
<comment>This brief message is displayed when a regular expression is invalid.</comment>
</data>
</root>
38 changes: 29 additions & 9 deletions src/cascadia/TerminalControl/SearchBoxControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -410,7 +416,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - <none>
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:
Expand All @@ -422,7 +428,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - <none>
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:
Expand Down Expand Up @@ -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),
Expand All @@ -532,9 +544,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - currentMatch - the index of the current match (0-based)
// Return Value:
// - <none>
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);
}

Expand Down
4 changes: 3 additions & 1 deletion src/cascadia/TerminalControl/SearchBoxControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*/);
Expand All @@ -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);

Expand Down
Loading

0 comments on commit 87c87b5

Please sign in to comment.