diff --git a/.github/actions/spell-check/dictionary/apis.txt b/.github/actions/spell-check/dictionary/apis.txt index ad4d0be0d..fddc4ddb7 100644 --- a/.github/actions/spell-check/dictionary/apis.txt +++ b/.github/actions/spell-check/dictionary/apis.txt @@ -53,6 +53,7 @@ rx serializer SIZENS spsc +sregex STDCPP strchr syscall @@ -62,6 +63,7 @@ tx UPDATEINIFILE userenv wcstoui +wsregex XDocument XElement XParse diff --git a/.github/actions/spell-check/dictionary/dictionary.txt b/.github/actions/spell-check/dictionary/dictionary.txt index 093c0ed43..23d5ba7cc 100644 --- a/.github/actions/spell-check/dictionary/dictionary.txt +++ b/.github/actions/spell-check/dictionary/dictionary.txt @@ -152433,6 +152433,7 @@ ft-lb ftncmd ftnerr FTP +ftp ft-pdl FTPI FTS diff --git a/.github/actions/spell-check/dictionary/names.txt b/.github/actions/spell-check/dictionary/names.txt index b56a1dfaf..dd4eaae18 100644 --- a/.github/actions/spell-check/dictionary/names.txt +++ b/.github/actions/spell-check/dictionary/names.txt @@ -8,6 +8,7 @@ dhowett Diviness dsafa duhowett +ekg ethanschoonover Firefox Gatta diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index 2c59fec6c..9128b389d 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -28,11 +28,13 @@ MD007: indent: 2 # Unordered list indentation MD013: line_length: 400 # Line length 80 is far to short +MD024: false # Allow multiple headings with same content MD026: punctuation: ".,;:!。,;:" # List of not allowed MD029: false # Ordered list item prefix MD033: false # Allow inline HTML MD036: false # Emphasis used instead of a heading +MD040: false # Allow ``` blocks in md files with no language specified ################# # Rules by tags # diff --git a/NOTICE.md b/NOTICE.md index e3c984a1f..d8fd3e9b0 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -2,7 +2,7 @@ Do Not Translate or Localize This software incorporates material from third parties. Microsoft makes certain -open source code available at http://3rdpartysource.microsoft.com, or you may +open source code available at [http://3rdpartysource.microsoft.com](http://3rdpartysource.microsoft.com), or you may send a check or money order for US $5.00, including the product name, the open source component name, and version number, to: @@ -20,7 +20,7 @@ General Public License. ## jsoncpp -**Source**: https://github.com/open-source-parsers/jsoncpp +**Source**: [https://github.com/open-source-parsers/jsoncpp](https://github.com/open-source-parsers/jsoncpp) ### License @@ -50,7 +50,7 @@ SOFTWARE. ## chromium/base/numerics -**Source**: https://github.com/chromium/chromium/tree/master/base/numerics +**Source**: [https://github.com/chromium/chromium/tree/master/base/numerics](https://github.com/chromium/chromium/tree/master/base/numerics) ### License @@ -86,7 +86,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## kimwalisch/libpopcnt -**Source**: https://github.com/kimwalisch/libpopcnt +**Source**: [https://github.com/kimwalisch/libpopcnt](https://github.com/kimwalisch/libpopcnt) ### License @@ -122,7 +122,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## dynamic_bitset -**Source**: https://github.com/pinam45/dynamic_bitset +**Source**: [https://github.com/pinam45/dynamic_bitset](https://github.com/pinam45/dynamic_bitset) ### License @@ -151,9 +151,9 @@ SOFTWARE. ``` -## {fmt} +## \{fmt\} -**Source**: https://github.com/fmtlib/fmt +**Source**: [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt) ### License @@ -188,3 +188,32 @@ of this Software are embedded into a machine-executable object form of such source code, you may redistribute such embedded portions in such object form without including the above copyright and permission notices. ``` + +## interval_tree + +**Source**: [https://github.com/ekg/IntervalTree](https://github.com/ekg/IntervalTree) + +### License + +``` +Copyright (c) 2011 Erik Garrison + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` diff --git a/oss/interval_tree/IntervalTree.h b/oss/interval_tree/IntervalTree.h new file mode 100644 index 000000000..372258bfc --- /dev/null +++ b/oss/interval_tree/IntervalTree.h @@ -0,0 +1,434 @@ +#ifndef __INTERVAL_TREE_H +#define __INTERVAL_TREE_H + +#include +#include +#include +#include +#include + +#ifdef USE_INTERVAL_TREE_NAMESPACE +namespace interval_tree +{ +#endif + template + class Interval + { + public: + Scalar start; + Scalar stop; + Value value; + Interval(const Scalar& s, const Scalar& e, const Value& v) : + start(std::min(s, e)), stop(std::max(s, e)), value(v) + { + } + + Interval() + { + } + + constexpr bool operator==(const Interval& other) const noexcept + { + return start == other.start && + stop == other.stop && + value == other.value; + } + + constexpr bool operator!=(const Interval& other) const noexcept + { + return !(*this == other); + } + }; + + template + Value intervalStart(const Interval& i) + { + return i.start; + } + + template + Value intervalStop(const Interval& i) + { + return i.stop; + } + + template + std::ostream& operator<<(std::ostream& out, const Interval& i) + { + out << "Interval(" << i.start << ", " << i.stop << "): " << i.value; + return out; + } + + template + class IntervalTree + { + public: + typedef Interval interval; + typedef std::vector interval_vector; + + struct IntervalStartCmp + { + bool operator()(const interval& a, const interval& b) + { + return a.start < b.start; + } + }; + + struct IntervalStopCmp + { + bool operator()(const interval& a, const interval& b) + { + return a.stop < b.stop; + } + }; + + IntervalTree() : + left(nullptr), right(nullptr), center() + { + } + + ~IntervalTree() = default; + + std::unique_ptr clone() const + { + return std::unique_ptr(new IntervalTree(*this)); + } + + IntervalTree(const IntervalTree& other) : + intervals(other.intervals), + left(other.left ? other.left->clone() : nullptr), + right(other.right ? other.right->clone() : nullptr), + center(other.center) + { + } + + IntervalTree& operator=(IntervalTree&&) = default; + IntervalTree(IntervalTree&&) = default; + + IntervalTree& operator=(const IntervalTree& other) + { + center = other.center; + intervals = other.intervals; + left = other.left ? other.left->clone() : nullptr; + right = other.right ? other.right->clone() : nullptr; + return *this; + } + + IntervalTree( + interval_vector&& ivals, + std::size_t depth = 16, + std::size_t minbucket = 64, + std::size_t maxbucket = 512, + Scalar leftextent = {}, + Scalar rightextent = {}) : + left(nullptr), right(nullptr) + { + --depth; + const auto minmaxStop = std::minmax_element(ivals.begin(), ivals.end(), IntervalStopCmp()); + const auto minmaxStart = std::minmax_element(ivals.begin(), ivals.end(), IntervalStartCmp()); + if (!ivals.empty()) + { + center = (minmaxStart.first->start + minmaxStop.second->stop) / 2; + } + if (leftextent == Scalar{} && rightextent == Scalar{}) + { + // sort intervals by start + std::sort(ivals.begin(), ivals.end(), IntervalStartCmp()); + } + else + { + assert(std::is_sorted(ivals.begin(), ivals.end(), IntervalStartCmp())); + } + if (depth == 0 || (ivals.size() < minbucket && ivals.size() < maxbucket)) + { + std::sort(ivals.begin(), ivals.end(), IntervalStartCmp()); + intervals = std::move(ivals); + assert(is_valid().first); + return; + } + else + { + Scalar leftp = Scalar{}; + Scalar rightp = Scalar{}; + + if (leftextent != Scalar{} || rightextent != Scalar{}) + { + leftp = leftextent; + rightp = rightextent; + } + else + { + leftp = ivals.front().start; + rightp = std::max_element(ivals.begin(), ivals.end(), IntervalStopCmp())->stop; + } + + interval_vector lefts; + interval_vector rights; + + for (typename interval_vector::const_iterator i = ivals.begin(); + i != ivals.end(); + ++i) + { + const interval& interval = *i; + if (interval.stop < center) + { + lefts.push_back(interval); + } + else if (interval.start > center) + { + rights.push_back(interval); + } + else + { + assert(interval.start <= center); + assert(center <= interval.stop); + intervals.push_back(interval); + } + } + + if (!lefts.empty()) + { + left.reset(new IntervalTree(std::move(lefts), + depth, + minbucket, + maxbucket, + leftp, + center)); + } + if (!rights.empty()) + { + right.reset(new IntervalTree(std::move(rights), + depth, + minbucket, + maxbucket, + center, + rightp)); + } + } + assert(is_valid().first); + } + + // Call f on all intervals near the range [start, stop]: + template + void visit_near(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + if (!intervals.empty() && !(stop < intervals.front().start)) + { + for (auto& i : intervals) + { + f(i); + } + } + if (left && start <= center) + { + left->visit_near(start, stop, f); + } + if (right && stop >= center) + { + right->visit_near(start, stop, f); + } + } + + // Call f on all intervals crossing pos + template + void visit_overlapping(const Scalar& pos, UnaryFunction f) const + { + visit_overlapping(pos, pos, f); + } + + // Call f on all intervals overlapping [start, stop] + template + void visit_overlapping(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + auto filterF = [&](const interval& interval) { + if (interval.stop >= start && interval.start <= stop) + { + // Only apply f if overlapping + f(interval); + } + }; + visit_near(start, stop, filterF); + } + + // Call f on all intervals contained within [start, stop] + template + void visit_contained(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + auto filterF = [&](const interval& interval) { + if (start <= interval.start && interval.stop <= stop) + { + f(interval); + } + }; + visit_near(start, stop, filterF); + } + + interval_vector findOverlapping(const Scalar& start, const Scalar& stop) const + { + interval_vector result; + visit_overlapping(start, stop, [&](const interval& interval) { + result.emplace_back(interval); + }); + return result; + } + + interval_vector findContained(const Scalar& start, const Scalar& stop) const + { + interval_vector result; + visit_contained(start, stop, [&](const interval& interval) { + result.push_back(interval); + }); + return result; + } + bool empty() const + { + if (left && !left->empty()) + { + return false; + } + if (!intervals.empty()) + { + return false; + } + if (right && !right->empty()) + { + return false; + } + return true; + } + + template + void visit_all(UnaryFunction f) const + { + if (left) + { + left->visit_all(f); + } + std::for_each(intervals.begin(), intervals.end(), f); + if (right) + { + right->visit_all(f); + } + } + + std::pair extentBruitForce() const + { + struct Extent + { + std::pair x = { std::numeric_limits::max(), + std::numeric_limits::min() }; + void operator()(const interval& interval) + { + x.first = std::min(x.first, interval.start); + x.second = std::max(x.second, interval.stop); + } + }; + Extent extent; + + visit_all([&](const interval& interval) { extent(interval); }); + return extent.x; + } + + // Check all constraints. + // If first is false, second is invalid. + std::pair> is_valid() const + { + const auto minmaxStop = std::minmax_element(intervals.begin(), intervals.end(), IntervalStopCmp()); + const auto minmaxStart = std::minmax_element(intervals.begin(), intervals.end(), IntervalStartCmp()); + + std::pair> result = { true, { std::numeric_limits::max(), std::numeric_limits::min() } }; + if (!intervals.empty()) + { + result.second.first = std::min(result.second.first, minmaxStart.first->start); + result.second.second = std::min(result.second.second, minmaxStop.second->stop); + } + if (left) + { + auto valid = left->is_valid(); + result.first &= valid.first; + result.second.first = std::min(result.second.first, valid.second.first); + result.second.second = std::min(result.second.second, valid.second.second); + if (!result.first) + { + return result; + } + if (valid.second.second >= center) + { + result.first = false; + return result; + } + } + if (right) + { + auto valid = right->is_valid(); + result.first &= valid.first; + result.second.first = std::min(result.second.first, valid.second.first); + result.second.second = std::min(result.second.second, valid.second.second); + if (!result.first) + { + return result; + } + if (valid.second.first <= center) + { + result.first = false; + return result; + } + } + if (!std::is_sorted(intervals.begin(), intervals.end(), IntervalStartCmp())) + { + result.first = false; + } + return result; + } + + friend std::ostream& operator<<(std::ostream& os, const IntervalTree& itree) + { + return writeOut(os, itree); + } + + friend std::ostream& writeOut(std::ostream& os, const IntervalTree& itree, std::size_t depth = 0) + { + auto pad = [&]() { for (std::size_t i = 0; i != depth; ++i) { os << ' '; } }; + pad(); + os << "center: " << itree.center << '\n'; + for (const interval& inter : itree.intervals) + { + pad(); + os << inter << '\n'; + } + if (itree.left) + { + pad(); + os << "left:\n"; + writeOut(os, *itree.left, depth + 1); + } + else + { + pad(); + os << "left: nullptr\n"; + } + if (itree.right) + { + pad(); + os << "right:\n"; + writeOut(os, *itree.right, depth + 1); + } + else + { + pad(); + os << "right: nullptr\n"; + } + return os; + } + + private: + interval_vector intervals; + std::unique_ptr left; + std::unique_ptr right; + Scalar center; + }; +#ifdef USE_INTERVAL_TREE_NAMESPACE +} +#endif + +#endif diff --git a/oss/interval_tree/MAINTAINER_README.md b/oss/interval_tree/MAINTAINER_README.md new file mode 100644 index 000000000..648db77d6 --- /dev/null +++ b/oss/interval_tree/MAINTAINER_README.md @@ -0,0 +1,17 @@ +# Notes for Future Maintainers + +This was originally imported by @PankajBhojwani in September 2020. + +The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme. +Please update the provenance information in that file when ingesting an updated version of the dependent library. +That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards. + +## What should be done to update this in the future? + +1. Go to ekg/intervaltreerepository on GitHub. +2. Take the file IntervalTree.h wholesale and drop it into the directory here. +3. Don't change anything about it. +4. Validate that the license in the root of the repository didn't change and update it if so. It is sitting in the same directory as this readme. + If it changed dramatically, ensure that it is still compatible with our license scheme. Also update the NOTICE file in the root of our repository to declare the third-party usage. +5. Submit the pull. + diff --git a/oss/interval_tree/cgmanifest.json b/oss/interval_tree/cgmanifest.json new file mode 100644 index 000000000..b6c5b2199 --- /dev/null +++ b/oss/interval_tree/cgmanifest.json @@ -0,0 +1,13 @@ +{"Registrations":[ + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/ekg/intervaltree", + "commitHash": "b90527f9e6d51cd36ecbb50429e4524d3a418ea5" + } + } + } + ], + "Version": 1 +} diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index bfb374e50..2c24936c2 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -14,6 +14,8 @@ using namespace Microsoft::Console; using namespace Microsoft::Console::Types; +using PointTree = interval_tree::IntervalTree; + // Routine Description: // - Creates a new instance of TextBuffer // Arguments: @@ -35,7 +37,8 @@ TextBuffer::TextBuffer(const COORD screenBufferSize, _unicodeStorage{}, _renderTarget{ renderTarget }, _size{}, - _currentHyperlinkId{ 1 } + _currentHyperlinkId{ 1 }, + _currentPatternId{ 0 } { // initialize ROWs for (size_t i = 0; i < static_cast(screenBufferSize.Y); ++i) @@ -2189,6 +2192,7 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, // Finish copying remaining parameters from the old text buffer to the new one newBuffer.CopyProperties(oldBuffer); newBuffer.CopyHyperlinkMaps(oldBuffer); + newBuffer.CopyPatterns(oldBuffer); // If we found where to put the cursor while placing characters into the buffer, // just put the cursor there. Otherwise we have to advance manually. @@ -2360,3 +2364,86 @@ void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other) _hyperlinkCustomIdMap = other._hyperlinkCustomIdMap; _currentHyperlinkId = other._currentHyperlinkId; } + +// Method Description: +// - Adds a regex pattern we should search for +// - The searching does not happen here, we only search when asked to by TerminalCore +// Arguments: +// - The regex pattern +// Return value: +// - An ID that the caller should associate with the given pattern +const size_t TextBuffer::AddPatternRecognizer(const std::wstring_view regexString) +{ + ++_currentPatternId; + _idsAndPatterns.emplace(std::make_pair(_currentPatternId, regexString)); + return _currentPatternId; +} + +// Method Description: +// - Copies the patterns the other buffer knows about into this one +// Arguments: +// - The other buffer +void TextBuffer::CopyPatterns(const TextBuffer& OtherBuffer) +{ + _idsAndPatterns = OtherBuffer._idsAndPatterns; + _currentPatternId = OtherBuffer._currentPatternId; +} + +// Method Description: +// - Finds patterns within the requested region of the text buffer +// Arguments: +// - The firstRow to start searching from +// - The lastRow to search +// Return value: +// - An interval tree containing the patterns found +PointTree TextBuffer::GetPatterns(const size_t firstRow, const size_t lastRow) const +{ + PointTree::interval_vector intervals; + + std::wstring concatAll; + const auto rowSize = GetRowByOffset(0).size(); + concatAll.reserve(rowSize * (lastRow - firstRow + 1)); + + // to deal with text that spans multiple lines, we will first concatenate + // all the text into one string and find the patterns in that string + for (auto i = firstRow; i <= lastRow; ++i) + { + auto row = GetRowByOffset(i); + concatAll += row.GetCharRow().GetText(); + } + + // for each pattern we know of, iterate through the string + for (const auto& idAndPattern : _idsAndPatterns) + { + std::wregex regexObj{ idAndPattern.second }; + + // search through the run with our regex object + auto words_begin = std::wsregex_iterator(concatAll.begin(), concatAll.end(), regexObj); + auto words_end = std::wsregex_iterator(); + + size_t lenUpToThis = 0; + for (auto i = words_begin; i != words_end; ++i) + { + // record the locations - + // when we find a match, the prefix is text that is between this + // match and the previous match, so we use the size of the prefix + // along with the size of the match to determine the locations + const auto prefixSize = i->prefix().str().size(); + const auto start = lenUpToThis + prefixSize; + const auto end = start + i->str().size(); + lenUpToThis = end; + + const til::point startCoord{ gsl::narrow(start % rowSize), gsl::narrow(start / rowSize) }; + const til::point endCoord{ gsl::narrow(end % rowSize), gsl::narrow(end / rowSize) }; + + // store the intervals + // NOTE: these intervals are relative to the VIEWPORT not the buffer + // Keeping these relative to the viewport for now because its the renderer + // that actually uses these locations and the renderer works relative to + // the viewport + intervals.push_back(PointTree::interval(startCoord, endCoord, idAndPattern.first)); + } + } + PointTree result(std::move(intervals)); + return result; +} diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index e9a12ebee..b1e0be1c7 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -182,6 +182,10 @@ public: const std::optional lastCharacterViewport, std::optional> positionInfo); + const size_t AddPatternRecognizer(const std::wstring_view regexString); + void CopyPatterns(const TextBuffer& OtherBuffer); + interval_tree::IntervalTree GetPatterns(const size_t firstRow, const size_t lastRow) const; + private: void _UpdateSize(); Microsoft::Console::Types::Viewport _size; @@ -229,6 +233,9 @@ private: void _PruneHyperlinks(); + std::unordered_map _idsAndPatterns; + size_t _currentPatternId; + #ifdef UNIT_TESTING friend class TextBufferTests; friend class UiaTextRangeTests; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index fc33c7603..aeaad09ea 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -35,6 +35,9 @@ constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); // The minimum delay between updating the TSF input control. constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); +// The minimum delay between updating the locations of regex patterns +constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); + DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::TerminalControl::CopyFormat); namespace winrt::Microsoft::Terminal::TerminalControl::implementation @@ -107,6 +110,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // This event is explicitly revoked in the destructor: does not need weak_ref auto onReceiveOutputFn = [this](const hstring str) { _terminal->Write(str); + _updatePatternLocations->Run(); }; _connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn); @@ -146,6 +150,16 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation TsfRedrawInterval, Dispatcher()); + _updatePatternLocations = std::make_shared>( + [weakThis = get_weak()]() { + if (auto control{ weakThis.get() }) + { + control->UpdatePatternLocations(); + } + }, + UpdatePatternLocationsInterval, + Dispatcher()); + _updateScrollBar = std::make_shared>( [weakThis = get_weak()](const auto& update) { if (auto control{ weakThis.get() }) @@ -1342,13 +1356,16 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _lastHoveredCell = terminalPos; const auto newId = _terminal->GetHyperlinkIdAtPosition(terminalPos); - // If the hyperlink ID changed, trigger a redraw all (so this will happen both when we move - // onto a link and when we move off a link) - if (newId != _lastHoveredId) + const auto newInterval = _terminal->GetHyperlinkIntervalFromPosition(terminalPos); + // If the hyperlink ID changed or the interval changed, trigger a redraw all + // (so this will happen both when we move onto a link and when we move off a link) + if (newId != _lastHoveredId || (newInterval != _lastHoveredInterval)) { - _renderEngine->UpdateHyperlinkHoveredId(newId); - _renderer->TriggerRedrawAll(); _lastHoveredId = newId; + _lastHoveredInterval = newInterval; + _renderEngine->UpdateHyperlinkHoveredId(newId); + _renderer->UpdateLastHoveredInterval(newInterval); + _renderer->TriggerRedrawAll(); } } } @@ -1534,6 +1551,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return _DoMouseWheel(location, modifiers, delta, state); } + // Method Description: + // - Tell TerminalCore to update its knowledge about the locations of visible regex patterns + // - We should call this (through the throttled function) when something causes the visible + // region to change, such as when new text enters the buffer or the viewport is scrolled + void TermControl::UpdatePatternLocations() + { + _terminal->UpdatePatterns(); + } + // Method Description: // - Adjust the opacity of the acrylic background in response to a mouse // scrolling event. @@ -1660,6 +1686,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return; } + // Clear the regex pattern tree so the renderer does not try to render them while scrolling + _terminal->ClearPatternTree(); + const auto newValue = static_cast(args.NewValue()); // This is a scroll event that wasn't initiated by the terminal @@ -1671,6 +1700,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _updateScrollBar->ModifyPending([](auto& update) { update.newValue.reset(); }); + + _updatePatternLocations->Run(); } // Method Description: @@ -2251,6 +2282,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return; } + // Clear the regex pattern tree so the renderer does not try to render them while scrolling + _terminal->ClearPatternTree(); + _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); ScrollBarUpdate update; @@ -2261,6 +2295,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation update.newValue = viewTop; _updateScrollBar->Run(update); + _updatePatternLocations->Run(); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3d16738e0..f73c9b124 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -118,6 +118,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation bool OnMouseWheel(const Windows::Foundation::Point location, const int32_t delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown); + void UpdatePatternLocations(); + ~TermControl(); Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); @@ -180,6 +182,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation std::shared_ptr> _tsfTryRedrawCanvas; + std::shared_ptr> _updatePatternLocations; + struct ScrollBarUpdate { std::optional newValue; @@ -213,6 +217,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Track the last hyperlink ID we hovered over uint16_t _lastHoveredId; + std::optional::interval> _lastHoveredInterval; + using Timestamp = uint64_t; // imported from WinUser diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index b067f34f1..10dc06c19 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -20,6 +20,8 @@ using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; using namespace Microsoft::Console::VirtualTerminal; +using PointTree = interval_tree::IntervalTree; + static std::wstring _KeyEventsToText(std::deque>& inEventsToWrite) { std::wstring wstr = L""; @@ -78,6 +80,10 @@ void Terminal::Create(COORD viewportSize, SHORT scrollbackLines, IRenderTarget& const TextAttribute attr{}; const UINT cursorSize = 12; _buffer = std::make_unique(bufferSize, attr, cursorSize, renderTarget); + // Add regex pattern recognizers to the buffer + // For now, we only add the URI regex pattern + std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" }; + _hyperlinkPatternId = _buffer->AddPatternRecognizer(linkPattern); } // Method Description: @@ -410,9 +416,9 @@ bool Terminal::IsTrackingMouseInput() const noexcept } // Method Description: -// - If the clicked text is a hyperlink, open it +// - Given a coord, get the URI at that location // Arguments: -// - The position of the clicked text +// - The position std::wstring Terminal::GetHyperlinkAtPosition(const COORD position) { auto attr = _buffer->GetCellDataAt(_ConvertToBufferCell(position))->TextAttr(); @@ -421,6 +427,22 @@ std::wstring Terminal::GetHyperlinkAtPosition(const COORD position) auto uri = _buffer->GetHyperlinkUriFromId(attr.GetHyperlinkId()); return uri; } + // also look through our known pattern locations in our pattern interval tree + const auto result = GetHyperlinkIntervalFromPosition(position); + if (result.has_value() && result->value == _hyperlinkPatternId) + { + const auto start = result->start; + const auto end = result->stop; + std::wstring uri; + + const auto startIter = _buffer->GetCellDataAt(_ConvertToBufferCell(start)); + const auto endIter = _buffer->GetCellDataAt(_ConvertToBufferCell(end)); + for (auto iter = startIter; iter != endIter; ++iter) + { + uri += iter->Chars(); + } + return uri; + } return {}; } @@ -435,6 +457,28 @@ uint16_t Terminal::GetHyperlinkIdAtPosition(const COORD position) return _buffer->GetCellDataAt(_ConvertToBufferCell(position))->TextAttr().GetHyperlinkId(); } +// Method description: +// - Given a position in a URI pattern, gets the start and end coordinates of the URI +// Arguments: +// - The position +// Return value: +// - The interval representing the start and end coordinates +std::optional Terminal::GetHyperlinkIntervalFromPosition(const COORD position) +{ + const auto results = _patternIntervalTree.findOverlapping(COORD{ position.X + 1, position.Y }, position); + if (results.size() > 0) + { + for (const auto& result : results) + { + if (result.value == _hyperlinkPatternId) + { + return result; + } + } + } + return std::nullopt; +} + // Method Description: // - Send this particular (non-character) key event to the terminal. // - The terminal will translate the key and the modifiers pressed into the @@ -598,6 +642,53 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro return handledDown || handledUp; } +// Method Description: +// - Invalidates the regions described in the given pattern tree for the rendering purposes +// Arguments: +// - The interval tree containing regions that need to be invalidated +void Terminal::_InvalidatePatternTree(interval_tree::IntervalTree& tree) +{ + const auto vis = _VisibleStartIndex(); + auto invalidate = [=](const PointTree::interval& interval) { + COORD startCoord{ gsl::narrow(interval.start.x()), gsl::narrow(interval.start.y() + vis) }; + COORD endCoord{ gsl::narrow(interval.stop.x()), gsl::narrow(interval.stop.y() + vis) }; + _InvalidateFromCoords(startCoord, endCoord); + }; + tree.visit_all(invalidate); +} + +// Method Description: +// - Given start and end coords, invalidates all the regions between them +// Arguments: +// - The start and end coords +void Terminal::_InvalidateFromCoords(const COORD start, const COORD end) +{ + if (start.Y == end.Y) + { + SMALL_RECT region{ start.X, start.Y, end.X, end.Y }; + _buffer->GetRenderTarget().TriggerRedraw(Viewport::FromInclusive(region)); + } + else + { + const auto rowSize = gsl::narrow(_buffer->GetRowByOffset(0).size()); + + // invalidate the first line + SMALL_RECT region{ start.X, start.Y, rowSize - 1, start.Y }; + _buffer->GetRenderTarget().TriggerRedraw(Viewport::FromInclusive(region)); + + if ((end.Y - start.Y) > 1) + { + // invalidate the lines in between the first and last line + region = til::rectangle(0, start.Y + 1, rowSize - 1, end.Y - 1); + _buffer->GetRenderTarget().TriggerRedraw(Viewport::FromInclusive(region)); + } + + // invalidate the last line + region = til::rectangle(0, end.Y, end.X, end.Y); + _buffer->GetRenderTarget().TriggerRedraw(Viewport::FromInclusive(region)); + } +} + // Method Description: // - Returns the keyboard's scan code for the given virtual key code. // Arguments: @@ -856,6 +947,9 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition) proposedCursorPosition.Y--; rowsPushedOffTopOfBuffer++; } + + // manually erase our pattern intervals since the locations have changed now + _patternIntervalTree = {}; } // Update Cursor Position @@ -1041,6 +1135,30 @@ bool Terminal::IsCursorBlinkingAllowed() const noexcept return cursor.IsBlinkingAllowed(); } +// Method Description: +// - Update our internal knowledge about where regex patterns are on the screen +// - This is called by TerminalControl (through a throttled function) when the visible +// region changes (for example by text entering the buffer or scrolling) +void Terminal::UpdatePatterns() noexcept +{ + auto lock = LockForWriting(); + auto oldTree = _patternIntervalTree; + _patternIntervalTree = _buffer->GetPatterns(_VisibleStartIndex(), _VisibleEndIndex()); + _InvalidatePatternTree(oldTree); + _InvalidatePatternTree(_patternIntervalTree); +} + +// Method Description: +// - Clears and invalidates the interval pattern tree +// - This is called to prevent the renderer from rendering patterns while the +// visible region is changing +void Terminal::ClearPatternTree() noexcept +{ + auto oldTree = _patternIntervalTree; + _patternIntervalTree = {}; + _InvalidatePatternTree(oldTree); +} + const std::optional Terminal::GetTabColor() const noexcept { return _tabColor; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index b1f6c25e9..c9b30378d 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -132,6 +132,7 @@ public: std::wstring GetHyperlinkAtPosition(const COORD position); uint16_t GetHyperlinkIdAtPosition(const COORD position); + std::optional::interval> GetHyperlinkIntervalFromPosition(const COORD position); #pragma endregion #pragma region IBaseData(base to IRenderData and IUiaData) @@ -161,6 +162,7 @@ public: const bool IsGridLineDrawingAllowed() noexcept override; const std::wstring GetHyperlinkUri(uint16_t id) const noexcept override; const std::wstring GetHyperlinkCustomId(uint16_t id) const noexcept override; + const std::vector GetPatternId(const COORD location) const noexcept override; #pragma endregion #pragma region IUiaData @@ -187,6 +189,9 @@ public: void SetCursorOn(const bool isOn); bool IsCursorBlinkingAllowed() const noexcept; + void UpdatePatterns() noexcept; + void ClearPatternTree() noexcept; + const std::optional GetTabColor() const noexcept; Microsoft::Console::Render::BlinkingState& GetBlinkingState() const noexcept; @@ -235,6 +240,8 @@ private: bool _altGrAliasing; bool _suppressApplicationTitle; + size_t _hyperlinkPatternId; + #pragma region Text Selection // a selection is represented as a range between two COORDs (start and end) // the pivot is the COORD that remains selected when you extend a selection in any direction @@ -276,6 +283,10 @@ private: // underneath them, while others would prefer to anchor it in place. // Either way, we should make this behavior controlled by a setting. + interval_tree::IntervalTree _patternIntervalTree; + void _InvalidatePatternTree(interval_tree::IntervalTree& tree); + void _InvalidateFromCoords(const COORD start, const COORD end); + // Since virtual keys are non-zero, you assume that this field is empty/invalid if it is. struct KeyEventCodes { diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 759e9a0db..212f7c768 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -133,6 +133,32 @@ const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uin return _buffer->GetCustomIdFromId(id); } +// Method Description: +// - Gets the regex pattern ids of a location +// Arguments: +// - The location +// Return value: +// - The pattern IDs of the location +const std::vector Terminal::GetPatternId(const COORD location) const noexcept +{ + // Look through our interval tree for this location + const auto intervals = _patternIntervalTree.findOverlapping(COORD{ location.X + 1, location.Y }, location); + if (intervals.size() == 0) + { + return {}; + } + else + { + std::vector result{}; + for (const auto& interval : intervals) + { + result.emplace_back(interval.value); + } + return result; + } + return {}; +} + std::vector Terminal::GetSelectionRects() noexcept try { diff --git a/src/common.build.pre.props b/src/common.build.pre.props index 20587e1f7..caf53f2a8 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -90,7 +90,7 @@ precomp.h $(IntDir)$(TargetName).pdb ProgramDatabase - $(SolutionDir)\src\inc;$(SolutionDir)\dep;$(SolutionDir)\dep\Console;$(SolutionDir)\dep\Win32K;$(SolutionDir)\dep\gsl\include;$(SolutionDir)\dep\wil\include;$(SolutionDir)\oss\chromium;$(SolutionDir)\oss\fmt\include;$(SolutionDir)\oss\dynamic_bitset;$(SolutionDir)\oss\libpopcnt;%(AdditionalIncludeDirectories); + $(SolutionDir)\src\inc;$(SolutionDir)\dep;$(SolutionDir)\dep\Console;$(SolutionDir)\dep\Win32K;$(SolutionDir)\dep\gsl\include;$(SolutionDir)\dep\wil\include;$(SolutionDir)\oss\chromium;$(SolutionDir)\oss\fmt\include;$(SolutionDir)\oss\dynamic_bitset;$(SolutionDir)\oss\libpopcnt;$(SolutionDir)\oss\interval_tree;%(AdditionalIncludeDirectories); true false false diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index 33ae6fa34..41d41d4d0 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -348,6 +348,12 @@ const std::wstring RenderData::GetHyperlinkCustomId(uint16_t id) const noexcept return gci.GetActiveOutputBuffer().GetTextBuffer().GetCustomIdFromId(id); } +// For now, we ignore regex patterns in conhost +const std::vector RenderData::GetPatternId(const COORD /*location*/) const noexcept +{ + return {}; +} + // Routine Description: // - Converts a text attribute into the RGB values that should be presented, applying // relevant table translation information and preferences. diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 2e8fa6879..26f291477 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -58,6 +58,8 @@ public: const std::wstring GetHyperlinkUri(uint16_t id) const noexcept override; const std::wstring GetHyperlinkCustomId(uint16_t id) const noexcept override; + + const std::vector GetPatternId(const COORD location) const noexcept override; #pragma endregion #pragma region IUiaData diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 0e20bbcb3..ef5afa3a5 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -401,6 +401,11 @@ public: { return {}; } + + const std::vector GetPatternId(const COORD /*location*/) const noexcept + { + return {}; + } }; void VtIoTests::RendererDtorAndThread() diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index e2837dc2c..1e2510f06 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -91,6 +91,9 @@ // {fmt}, a C++20-compatible formatting library #include +#define USE_INTERVAL_TREE_NAMESPACE +#include + // SAL #include diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 6d7f40a36..87c802ebb 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -123,6 +123,38 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } } + constexpr bool operator<=(const point& other) const noexcept + { + if (_y < other._y) + { + return true; + } + else if (_y > other._y) + { + return false; + } + else + { + return _x <= other._x; + } + } + + constexpr bool operator>=(const point& other) const noexcept + { + if (_y > other._y) + { + return true; + } + else if (_y < other._y) + { + return false; + } + else + { + return _x >= other._x; + } + } + point operator+(const point& other) const { ptrdiff_t x; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 0bff7c42d..7fd64f169 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -10,6 +10,8 @@ using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; +using PointTree = interval_tree::IntervalTree; + static constexpr auto maxRetriesForRenderEngine = 3; // The renderer will wait this number of milliseconds * how many tries have elapsed before trying again. static constexpr auto renderBackoffBaseTimeMilliseconds{ 150 }; @@ -693,6 +695,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // Retrieve the first color. auto color = it->TextAttr(); + // Retrieve the first pattern id + auto patternIds = _pData->GetPatternId(target); // And hold the point where we should start drawing. auto screenPoint = target; @@ -706,6 +710,9 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // when we go to draw gridlines for the length of the run. const auto currentRunColor = color; + // Hold onto the current pattern id as well + const auto currentPatternId = patternIds; + // Update the drawing brushes with our color. THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false)); @@ -731,16 +738,20 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // This inner loop will accumulate clusters until the color changes. // When the color changes, it will save the new color off and break. + // We also accumulate clusters according to regex patterns do { - if (color != it->TextAttr()) + COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; + const auto thisPointPatterns = _pData->GetPatternId(thisPoint); + if (color != it->TextAttr() || patternIds != thisPointPatterns) { auto newAttr{ it->TextAttr() }; // foreground doesn't matter for runs of spaces (!) // if we trick it . . . we call Paint far fewer times for cmatrix - if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert)) + if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || patternIds != thisPointPatterns) { color = newAttr; + patternIds = thisPointPatterns; break; // vend this run } } @@ -900,6 +911,22 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin { // Convert console grid line representations into rendering engine enum representations. IRenderEngine::GridLines lines = Renderer::s_GetGridlines(textAttribute); + + // For now, we dash underline patterns and switch to regular underline on hover + if (_pData->GetPatternId(coordTarget).size() > 0) + { + if (_hoveredInterval.has_value() && + _hoveredInterval.value().start <= til::point{ coordTarget } && + til::point{ coordTarget } <= _hoveredInterval.value().stop) + { + lines |= IRenderEngine::GridLines::Underline; + } + else + { + lines |= IRenderEngine::GridLines::HyperlinkUnderline; + } + } + // Return early if there are no lines to paint. if (lines != IRenderEngine::GridLines::None) { @@ -1216,6 +1243,11 @@ void Renderer::ResetErrorStateAndResume() EnablePainting(); } +void Renderer::UpdateLastHoveredInterval(const std::optional& newInterval) +{ + _hoveredInterval = newInterval; +} + // Method Description: // - Blocks until the engines are able to render without blocking. void Renderer::WaitUntilCanRender() diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index db6dc6d38..ba9963e4e 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -80,6 +80,8 @@ namespace Microsoft::Console::Render void SetRendererEnteredErrorStateCallback(std::function pfn); void ResetErrorStateAndResume(); + void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); + private: std::deque _rgpEngines; @@ -88,6 +90,8 @@ namespace Microsoft::Console::Render std::unique_ptr _pThread; bool _destructing = false; + std::optional::interval> _hoveredInterval; + void _NotifyPaintFrame(); [[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept; diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index 11b347e65..47da94b8d 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -69,6 +69,8 @@ namespace Microsoft::Console::Render virtual const std::wstring GetHyperlinkUri(uint16_t id) const noexcept = 0; virtual const std::wstring GetHyperlinkCustomId(uint16_t id) const noexcept = 0; + virtual const std::vector GetPatternId(const COORD location) const noexcept = 0; + protected: IRenderData() = default; }; diff --git a/src/til/ut_til/PointTests.cpp b/src/til/ut_til/PointTests.cpp index 496fbc65d..00a25b193 100644 --- a/src/til/ut_til/PointTests.cpp +++ b/src/til/ut_til/PointTests.cpp @@ -175,6 +175,82 @@ class PointTests } } + TEST_METHOD(LessThanOrEqual) + { + Log::Comment(L"0.) Equal."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 <= s2); + } + + Log::Comment(L"1.) Left Width changed."); + { + const til::point s1{ 4, 10 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 <= s2); + } + + Log::Comment(L"2.) Right Width changed."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 6, 10 }; + VERIFY_IS_TRUE(s1 <= s2); + } + + Log::Comment(L"3.) Left Height changed."); + { + const til::point s1{ 5, 9 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 <= s2); + } + + Log::Comment(L"4.) Right Height changed."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 5, 11 }; + VERIFY_IS_TRUE(s1 <= s2); + } + } + + TEST_METHOD(GreaterThanOrEqual) + { + Log::Comment(L"0.) Equal."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 >= s2); + } + + Log::Comment(L"1.) Left Width changed."); + { + const til::point s1{ 4, 10 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_FALSE(s1 >= s2); + } + + Log::Comment(L"2.) Right Width changed."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 6, 10 }; + VERIFY_IS_FALSE(s1 >= s2); + } + + Log::Comment(L"3.) Left Height changed."); + { + const til::point s1{ 5, 9 }; + const til::point s2{ 5, 10 }; + VERIFY_IS_FALSE(s1 >= s2); + } + + Log::Comment(L"4.) Right Height changed."); + { + const til::point s1{ 5, 10 }; + const til::point s2{ 5, 11 }; + VERIFY_IS_FALSE(s1 >= s2); + } + } + TEST_METHOD(Addition) { Log::Comment(L"0.) Addition of two things that should be in bounds.");