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:
parent
8e3f27f8fb
commit
2bf5d18c84
|
@ -53,6 +53,7 @@ rx
|
|||
serializer
|
||||
SIZENS
|
||||
spsc
|
||||
sregex
|
||||
STDCPP
|
||||
strchr
|
||||
syscall
|
||||
|
@ -62,6 +63,7 @@ tx
|
|||
UPDATEINIFILE
|
||||
userenv
|
||||
wcstoui
|
||||
wsregex
|
||||
XDocument
|
||||
XElement
|
||||
XParse
|
||||
|
|
|
@ -152433,6 +152433,7 @@ ft-lb
|
|||
ftncmd
|
||||
ftnerr
|
||||
FTP
|
||||
ftp
|
||||
ft-pdl
|
||||
FTPI
|
||||
FTS
|
||||
|
|
|
@ -8,6 +8,7 @@ dhowett
|
|||
Diviness
|
||||
dsafa
|
||||
duhowett
|
||||
ekg
|
||||
ethanschoonover
|
||||
Firefox
|
||||
Gatta
|
||||
|
|
|
@ -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 #
|
||||
|
|
43
NOTICE.md
43
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.
|
||||
|
||||
```
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{"Registrations":[
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"repositoryUrl": "https://github.com/ekg/intervaltree",
|
||||
"commitHash": "b90527f9e6d51cd36ecbb50429e4524d3a418ea5"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": 1
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -401,6 +401,11 @@ public:
|
|||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::vector<size_t> GetPatternId(const COORD /*location*/) const noexcept
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
void VtIoTests::RendererDtorAndThread()
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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.");
|
||||
|
|
Loading…
Reference in New Issue