Skip to content

Commit 87c87b5

Browse files
committed
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 ecb5631) Service-Card-Id: PVTI_lADOAF3p4s4AmhmszgTTM0g Service-Version: 1.21
1 parent dc8a830 commit 87c87b5

File tree

24 files changed

+203
-63
lines changed

24 files changed

+203
-63
lines changed

.github/actions/spelling/expect/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ FILTERONPASTE
607607
FINDCASE
608608
FINDDLG
609609
FINDDOWN
610+
FINDREGEX
610611
FINDSTRINGEXACT
611612
FINDUP
612613
FIter

src/buffer/out/search.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,26 @@
88

99
using namespace Microsoft::Console::Types;
1010

11-
bool Search::IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept
11+
bool Search::IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags) const noexcept
1212
{
1313
return _renderData != &renderData ||
1414
_needle != needle ||
15-
_caseInsensitive != caseInsensitive ||
15+
_flags != flags ||
1616
_lastMutationId != renderData.GetTextBuffer().GetLastMutationId();
1717
}
1818

19-
bool Search::Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse)
19+
bool Search::Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags, bool reverse)
2020
{
2121
const auto& textBuffer = renderData.GetTextBuffer();
2222

2323
_renderData = &renderData;
2424
_needle = needle;
25-
_caseInsensitive = caseInsensitive;
25+
_flags = flags;
2626
_lastMutationId = textBuffer.GetLastMutationId();
2727

28-
_results = textBuffer.SearchText(needle, caseInsensitive);
28+
auto result = textBuffer.SearchText(needle, _flags);
29+
_ok = result.has_value();
30+
_results = std::move(result).value_or(std::vector<til::point_span>{});
2931
_index = reverse ? gsl::narrow_cast<ptrdiff_t>(_results.size()) - 1 : 0;
3032
_step = reverse ? -1 : 1;
3133
return true;
@@ -144,3 +146,8 @@ ptrdiff_t Search::CurrentMatch() const noexcept
144146
{
145147
return _index;
146148
}
149+
150+
bool Search::IsOk() const noexcept
151+
{
152+
return _ok;
153+
}

src/buffer/out/search.h

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,23 @@ Revision History:
2020
#include "textBuffer.hpp"
2121
#include "../renderer/inc/IRenderData.hpp"
2222

23+
enum class SearchFlag : unsigned int
24+
{
25+
None = 0,
26+
27+
CaseInsensitive = 1 << 0,
28+
RegularExpression = 1 << 1,
29+
};
30+
31+
DEFINE_ENUM_FLAG_OPERATORS(SearchFlag);
32+
2333
class Search final
2434
{
2535
public:
2636
Search() = default;
2737

28-
bool IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive) const noexcept;
29-
bool Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool caseInsensitive, bool reverse);
38+
bool IsStale(const Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags) const noexcept;
39+
bool Reset(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, SearchFlag flags, bool reverse);
3040

3141
void MoveToCurrentSelection();
3242
void MoveToPoint(til::point anchor) noexcept;
@@ -39,14 +49,16 @@ class Search final
3949
const std::vector<til::point_span>& Results() const noexcept;
4050
std::vector<til::point_span>&& ExtractResults() noexcept;
4151
ptrdiff_t CurrentMatch() const noexcept;
52+
bool IsOk() const noexcept;
4253

4354
private:
4455
// _renderData is a pointer so that Search() is constexpr default constructable.
4556
Microsoft::Console::Render::IRenderData* _renderData = nullptr;
4657
std::wstring _needle;
47-
bool _caseInsensitive = false;
58+
SearchFlag _flags{};
4859
uint64_t _lastMutationId = 0;
4960

61+
bool _ok{ false };
5062
std::vector<til::point_span> _results;
5163
ptrdiff_t _index = 0;
5264
ptrdiff_t _step = 0;

src/buffer/out/textBuffer.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "../../types/inc/GlyphWidth.hpp"
1313
#include "../renderer/base/renderer.hpp"
1414
#include "../types/inc/utils.hpp"
15+
#include "search.h"
1516

1617
using namespace Microsoft::Console;
1718
using namespace Microsoft::Console::Types;
@@ -3193,14 +3194,15 @@ void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other)
31933194

31943195
// Searches through the entire (committed) text buffer for `needle` and returns the coordinates in absolute coordinates.
31953196
// The end coordinates of the returned ranges are considered inclusive.
3196-
std::vector<til::point_span> TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive) const
3197+
std::optional<std::vector<til::point_span>> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags) const
31973198
{
3198-
return SearchText(needle, caseInsensitive, 0, til::CoordTypeMax);
3199+
return SearchText(needle, flags, 0, til::CoordTypeMax);
31993200
}
32003201

32013202
// Searches through the given rows [rowBeg,rowEnd) for `needle` and returns the coordinates in absolute coordinates.
32023203
// While the end coordinates of the returned ranges are considered inclusive, the [rowBeg,rowEnd) range is half-open.
3203-
std::vector<til::point_span> TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const
3204+
// Returns nullopt if the parameters were invalid (e.g. regex search was requested with an invalid regex)
3205+
std::optional<std::vector<til::point_span>> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const
32043206
{
32053207
rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1);
32063208

@@ -3214,11 +3216,25 @@ std::vector<til::point_span> TextBuffer::SearchText(const std::wstring_view& nee
32143216

32153217
auto text = ICU::UTextFromTextBuffer(*this, rowBeg, rowEnd);
32163218

3217-
uint32_t flags = UREGEX_LITERAL;
3218-
WI_SetFlagIf(flags, UREGEX_CASE_INSENSITIVE, caseInsensitive);
3219+
uint32_t icuFlags{ 0 };
3220+
WI_SetFlagIf(icuFlags, UREGEX_CASE_INSENSITIVE, WI_IsFlagSet(flags, SearchFlag::CaseInsensitive));
3221+
3222+
if (WI_IsFlagSet(flags, SearchFlag::RegularExpression))
3223+
{
3224+
WI_SetFlag(icuFlags, UREGEX_MULTILINE);
3225+
}
3226+
else
3227+
{
3228+
WI_SetFlag(icuFlags, UREGEX_LITERAL);
3229+
}
32193230

32203231
UErrorCode status = U_ZERO_ERROR;
3221-
const auto re = ICU::CreateRegex(needle, flags, &status);
3232+
const auto re = ICU::CreateRegex(needle, icuFlags, &status);
3233+
if (status > U_ZERO_ERROR)
3234+
{
3235+
return std::nullopt;
3236+
}
3237+
32223238
uregex_setUText(re.get(), &text, &status);
32233239

32243240
if (uregex_find(re.get(), -1, &status))

src/buffer/out/textBuffer.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ filling in the last row, and updating the screen.
5858
#include "../buffer/out/textBufferTextIterator.hpp"
5959

6060
struct URegularExpression;
61+
enum class SearchFlag : unsigned int;
6162

6263
namespace Microsoft::Console::Render
6364
{
@@ -293,8 +294,8 @@ class TextBuffer final
293294

294295
static void Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Microsoft::Console::Types::Viewport* lastCharacterViewport = nullptr, PositionInformation* positionInfo = nullptr);
295296

296-
std::vector<til::point_span> SearchText(const std::wstring_view& needle, bool caseInsensitive) const;
297-
std::vector<til::point_span> SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const;
297+
std::optional<std::vector<til::point_span>> SearchText(const std::wstring_view& needle, SearchFlag flags) const;
298+
std::optional<std::vector<til::point_span>> SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const;
298299

299300
// Mark handling
300301
std::vector<ScrollMark> GetMarkRows() const;

src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "WexTestClass.h"
77
#include "../textBuffer.hpp"
88
#include "../../renderer/inc/DummyRenderer.hpp"
9+
#include "../search.h"
910

1011
template<>
1112
class WEX::TestExecution::VerifyOutputTraits<std::vector<til::point_span>>
@@ -49,15 +50,15 @@ class UTextAdapterTests
4950
};
5051

5152
auto expected = std::vector{ s(0, 2), s(8, 10) };
52-
auto actual = buffer.SearchText(L"abc", false);
53+
auto actual = buffer.SearchText(L"abc", SearchFlag::None);
5354
VERIFY_ARE_EQUAL(expected, actual);
5455

5556
expected = std::vector{ s(5, 5) };
56-
actual = buffer.SearchText(L"𝒷", false);
57+
actual = buffer.SearchText(L"𝒷", SearchFlag::None);
5758
VERIFY_ARE_EQUAL(expected, actual);
5859

5960
expected = std::vector{ s(12, 15) };
60-
actual = buffer.SearchText(L"ネコ", false);
61+
actual = buffer.SearchText(L"ネコ", SearchFlag::None);
6162
VERIFY_ARE_EQUAL(expected, actual);
6263
}
6364
};

src/cascadia/TerminalControl/ControlCore.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,10 +1654,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
16541654
// - resetOnly: If true, only Reset() will be called, if anything. FindNext() will never be called.
16551655
// Return Value:
16561656
// - <none>
1657-
SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool resetOnly)
1657+
SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool regularExpression, const bool resetOnly)
16581658
{
16591659
const auto lock = _terminal->LockForWriting();
1660-
const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), text, !caseSensitive);
1660+
SearchFlag flags{};
1661+
WI_SetFlagIf(flags, SearchFlag::CaseInsensitive, !caseSensitive);
1662+
WI_SetFlagIf(flags, SearchFlag::RegularExpression, regularExpression);
1663+
const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), text, flags);
16611664

16621665
if (searchInvalidated || !resetOnly)
16631666
{
@@ -1666,7 +1669,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
16661669
if (searchInvalidated)
16671670
{
16681671
oldResults = _searcher.ExtractResults();
1669-
_searcher.Reset(*_terminal.get(), text, !caseSensitive, !goForward);
1672+
_searcher.Reset(*_terminal.get(), text, flags, !goForward);
16701673

16711674
if (SnapSearchResultToSelection())
16721675
{
@@ -1700,6 +1703,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
17001703
.TotalMatches = totalMatches,
17011704
.CurrentMatch = currentMatch,
17021705
.SearchInvalidated = searchInvalidated,
1706+
.SearchRegexInvalid = !_searcher.IsOk(),
17031707
};
17041708
}
17051709

src/cascadia/TerminalControl/ControlCore.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
219219
void SetSelectionAnchor(const til::point position);
220220
void SetEndSelectionPoint(const til::point position);
221221

222-
SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool reset);
222+
SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool regularExpression, bool reset);
223223
const std::vector<til::point_span>& SearchResultRows() const noexcept;
224224
void ClearSearch();
225225
void SnapSearchResultToSelection(bool snap) noexcept;

src/cascadia/TerminalControl/ControlCore.idl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ namespace Microsoft.Terminal.Control
5454
Int32 TotalMatches;
5555
Int32 CurrentMatch;
5656
Boolean SearchInvalidated;
57+
Boolean SearchRegexInvalid;
5758
};
5859

5960
[default_interface] runtimeclass SelectionColor
@@ -134,7 +135,7 @@ namespace Microsoft.Terminal.Control
134135
void ResumeRendering();
135136
void BlinkAttributeTick();
136137

137-
SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean reset);
138+
SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean regularExpression, Boolean reset);
138139
void ClearSearch();
139140
Boolean SnapSearchResultToSelection;
140141

src/cascadia/TerminalControl/Resources/en-US/Resources.resw

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,16 @@ Please either install the missing font or choose another one.</value>
304304
<value>Restored</value>
305305
<comment>"Restored" as in "This content was restored"</comment>
306306
</data>
307-
</root>
307+
<data name="SearchBox_RegularExpression.ToolTipService.ToolTip" xml:space="preserve">
308+
<value>Regular Expression</value>
309+
<comment>The tooltip text for the button on the search box control governing the use of "regular expressions" ("regex").</comment>
310+
</data>
311+
<data name="SearchBox_RegularExpression.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
312+
<value>Regular Expression Search</value>
313+
<comment>The accessibility description text for the button on the search box control governing the use of "regular expressions" ("regex").</comment>
314+
</data>
315+
<data name="SearchRegexInvalid" xml:space="preserve">
316+
<value>invalid</value>
317+
<comment>This brief message is displayed when a regular expression is invalid.</comment>
318+
</data>
319+
</root>

0 commit comments

Comments
 (0)