Add support for autodetecting URLs and making hyperlinks (#7691)

This pull request is the initial implementation of hyperlink auto
detection

Overall design:
- Upon startup, TerminalCore gives the TextBuffer some patterns it
  should know about
- Whenever something in the viewport changes (i.e. text
  output/scrolling), TerminalControl tells TerminalCore (through a
  throttled function for performance) to retrieve the visible pattern
  locations from the TextBuffer
- When the renderer encounters a region that is associated with a
  pattern, it paints that region differently 

References #5001
Closes #574
This commit is contained in:
PankajBhojwani 2020-10-28 16:24:43 -04:00 committed by GitHub
parent 8e3f27f8fb
commit 2bf5d18c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 969 additions and 18 deletions

View File

@ -53,6 +53,7 @@ rx
serializer
SIZENS
spsc
sregex
STDCPP
strchr
syscall
@ -62,6 +63,7 @@ tx
UPDATEINIFILE
userenv
wcstoui
wsregex
XDocument
XElement
XParse

View File

@ -152433,6 +152433,7 @@ ft-lb
ftncmd
ftnerr
FTP
ftp
ft-pdl
FTPI
FTS

View File

@ -8,6 +8,7 @@ dhowett
Diviness
dsafa
duhowett
ekg
ethanschoonover
Firefox
Gatta

View File

@ -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 #

View File

@ -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.
```

View File

@ -0,0 +1,434 @@
#ifndef __INTERVAL_TREE_H
#define __INTERVAL_TREE_H
#include <vector>
#include <algorithm>
#include <iostream>
#include <memory>
#include <cassert>
#ifdef USE_INTERVAL_TREE_NAMESPACE
namespace interval_tree
{
#endif
template<class Scalar, typename Value>
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<class Scalar, typename Value>
Value intervalStart(const Interval<Scalar, Value>& i)
{
return i.start;
}
template<class Scalar, typename Value>
Value intervalStop(const Interval<Scalar, Value>& i)
{
return i.stop;
}
template<class Scalar, typename Value>
std::ostream& operator<<(std::ostream& out, const Interval<Scalar, Value>& i)
{
out << "Interval(" << i.start << ", " << i.stop << "): " << i.value;
return out;
}
template<class Scalar, class Value>
class IntervalTree
{
public:
typedef Interval<Scalar, Value> interval;
typedef std::vector<interval> 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<IntervalTree> clone() const
{
return std::unique_ptr<IntervalTree>(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<class UnaryFunction>
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<class UnaryFunction>
void visit_overlapping(const Scalar& pos, UnaryFunction f) const
{
visit_overlapping(pos, pos, f);
}
// Call f on all intervals overlapping [start, stop]
template<class UnaryFunction>
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<class UnaryFunction>
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<class UnaryFunction>
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<Scalar, Scalar> extentBruitForce() const
{
struct Extent
{
std::pair<Scalar, Scalar> x = { std::numeric_limits<Scalar>::max(),
std::numeric_limits<Scalar>::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<bool, std::pair<Scalar, Scalar>> 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<bool, std::pair<Scalar, Scalar>> result = { true, { std::numeric_limits<Scalar>::max(), std::numeric_limits<Scalar>::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<IntervalTree> left;
std::unique_ptr<IntervalTree> right;
Scalar center;
};
#ifdef USE_INTERVAL_TREE_NAMESPACE
}
#endif
#endif

View File

@ -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.

View File

@ -0,0 +1,13 @@
{"Registrations":[
{
"component": {
"type": "git",
"git": {
"repositoryUrl": "https://github.com/ekg/intervaltree",
"commitHash": "b90527f9e6d51cd36ecbb50429e4524d3a418ea5"
}
}
}
],
"Version": 1
}

View File

@ -14,6 +14,8 @@
using namespace Microsoft::Console;
using namespace Microsoft::Console::Types;
using PointTree = interval_tree::IntervalTree<til::point, size_t>;
// 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<size_t>(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<SHORT>(start % rowSize), gsl::narrow<SHORT>(start / rowSize) };
const til::point endCoord{ gsl::narrow<SHORT>(end % rowSize), gsl::narrow<SHORT>(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;
}

View File

@ -182,6 +182,10 @@ public:
const std::optional<Microsoft::Console::Types::Viewport> lastCharacterViewport,
std::optional<std::reference_wrapper<PositionInformation>> positionInfo);
const size_t AddPatternRecognizer(const std::wstring_view regexString);
void CopyPatterns(const TextBuffer& OtherBuffer);
interval_tree::IntervalTree<til::point, size_t> 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<size_t, std::wstring> _idsAndPatterns;
size_t _currentPatternId;
#ifdef UNIT_TESTING
friend class TextBufferTests;
friend class UiaTextRangeTests;

View File

@ -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<ThrottledFunc<>>(
[weakThis = get_weak()]() {
if (auto control{ weakThis.get() })
{
control->UpdatePatternLocations();
}
},
UpdatePatternLocationsInterval,
Dispatcher());
_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
[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<int>(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:

View File

@ -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<ThrottledFunc<>> _tsfTryRedrawCanvas;
std::shared_ptr<ThrottledFunc<>> _updatePatternLocations;
struct ScrollBarUpdate
{
std::optional<double> 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_tree::IntervalTree<til::point, size_t>::interval> _lastHoveredInterval;
using Timestamp = uint64_t;
// imported from WinUser

View File

@ -20,6 +20,8 @@ using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::VirtualTerminal;
using PointTree = interval_tree::IntervalTree<til::point, size_t>;
static std::wstring _KeyEventsToText(std::deque<std::unique_ptr<IInputEvent>>& 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<TextBuffer>(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<PointTree::interval> 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<til::point, size_t>& tree)
{
const auto vis = _VisibleStartIndex();
auto invalidate = [=](const PointTree::interval& interval) {
COORD startCoord{ gsl::narrow<SHORT>(interval.start.x()), gsl::narrow<SHORT>(interval.start.y() + vis) };
COORD endCoord{ gsl::narrow<SHORT>(interval.stop.x()), gsl::narrow<SHORT>(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<SHORT>(_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<til::color> Terminal::GetTabColor() const noexcept
{
return _tabColor;

View File

@ -132,6 +132,7 @@ public:
std::wstring GetHyperlinkAtPosition(const COORD position);
uint16_t GetHyperlinkIdAtPosition(const COORD position);
std::optional<interval_tree::IntervalTree<til::point, size_t>::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<size_t> 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<til::color> 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<til::point, size_t> _patternIntervalTree;
void _InvalidatePatternTree(interval_tree::IntervalTree<til::point, size_t>& 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
{

View File

@ -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<size_t> 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<size_t> result{};
for (const auto& interval : intervals)
{
result.emplace_back(interval.value);
}
return result;
}
return {};
}
std::vector<Microsoft::Console::Types::Viewport> Terminal::GetSelectionRects() noexcept
try
{

View File

@ -90,7 +90,7 @@
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
<ProgramDataBaseFileName>$(IntDir)$(TargetName).pdb</ProgramDataBaseFileName>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<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;%(AdditionalIncludeDirectories);</AdditionalIncludeDirectories>
<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);</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<MinimalRebuild>false</MinimalRebuild>
<RuntimeTypeInfo>false</RuntimeTypeInfo>

View File

@ -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<size_t> 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.

View File

@ -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<size_t> GetPatternId(const COORD location) const noexcept override;
#pragma endregion
#pragma region IUiaData

View File

@ -401,6 +401,11 @@ public:
{
return {};
}
const std::vector<size_t> GetPatternId(const COORD /*location*/) const noexcept
{
return {};
}
};
void VtIoTests::RendererDtorAndThread()

View File

@ -91,6 +91,9 @@
// {fmt}, a C++20-compatible formatting library
#include <fmt/format.h>
#define USE_INTERVAL_TREE_NAMESPACE
#include <IntervalTree.h>
// SAL
#include <sal.h>

View File

@ -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;

View File

@ -10,6 +10,8 @@
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
using PointTree = interval_tree::IntervalTree<til::point, size_t>;
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<SHORT>(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<PointTree::interval>& newInterval)
{
_hoveredInterval = newInterval;
}
// Method Description:
// - Blocks until the engines are able to render without blocking.
void Renderer::WaitUntilCanRender()

View File

@ -80,6 +80,8 @@ namespace Microsoft::Console::Render
void SetRendererEnteredErrorStateCallback(std::function<void()> pfn);
void ResetErrorStateAndResume();
void UpdateLastHoveredInterval(const std::optional<interval_tree::IntervalTree<til::point, size_t>::interval>& newInterval);
private:
std::deque<IRenderEngine*> _rgpEngines;
@ -88,6 +90,8 @@ namespace Microsoft::Console::Render
std::unique_ptr<IRenderThread> _pThread;
bool _destructing = false;
std::optional<interval_tree::IntervalTree<til::point, size_t>::interval> _hoveredInterval;
void _NotifyPaintFrame();
[[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept;

View File

@ -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<size_t> GetPatternId(const COORD location) const noexcept = 0;
protected:
IRenderData() = default;
};

View File

@ -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.");