Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for autodetecting URLs and making hyperlinks #7691

Merged
53 commits merged into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
481c16e
rough implementation
PankajBhojwani Sep 21, 2020
062e780
spell/format
PankajBhojwani Sep 21, 2020
973ed97
const/c cast fix
PankajBhojwani Sep 21, 2020
dbb9a9d
initialize inc
PankajBhojwani Sep 21, 2020
e4e5858
prototype
PankajBhojwani Sep 25, 2020
4db85a4
const
PankajBhojwani Sep 25, 2020
137fcb9
detected links now clickable
PankajBhojwani Sep 25, 2020
8b95bd9
interval trees
PankajBhojwani Sep 28, 2020
d1b2db3
spell
PankajBhojwani Sep 28, 2020
89b3c5a
using legal interval tree implementation
PankajBhojwani Sep 29, 2020
1297c15
spelling and date
PankajBhojwani Sep 29, 2020
68d2c4c
license things
PankajBhojwani Sep 29, 2020
b3f45d9
spellllll
PankajBhojwani Sep 29, 2020
c88ab2d
terminal universal
PankajBhojwani Sep 29, 2020
4692b9b
fix comments/some optimizations
PankajBhojwani Sep 30, 2020
9f1a5bc
invalidate regions
PankajBhojwani Sep 30, 2020
9d88aa5
renaming
PankajBhojwani Oct 1, 2020
9211952
addressing comments
PankajBhojwani Oct 1, 2020
9ff827f
interval tree of points
PankajBhojwani Oct 1, 2020
2256cac
small fix
PankajBhojwani Oct 1, 2020
27e2b3f
const
PankajBhojwani Oct 1, 2020
13df0dc
double underline on hover
PankajBhojwani Oct 2, 2020
d45a4d9
dashed underline for patterns, solid underline on hover
PankajBhojwani Oct 5, 2020
3f79842
some cleaning up
PankajBhojwani Oct 5, 2020
0c324b0
more cleanup
PankajBhojwani Oct 5, 2020
06b4a98
render data returns vector of pattern ids
PankajBhojwani Oct 5, 2020
99b42a6
interval tree equality redundancy
PankajBhojwani Oct 5, 2020
aced0e4
fix rendering issue
PankajBhojwani Oct 6, 2020
3d4d4c7
typedef fixes, tests
PankajBhojwani Oct 8, 2020
f3083df
Merge branch 'master' of https://github.com/microsoft/terminal into d…
PankajBhojwani Oct 12, 2020
a6a6aa2
update patterns when scroll bar changes, lock for writing
PankajBhojwani Oct 12, 2020
ce579ea
format
PankajBhojwani Oct 12, 2020
e2535ad
erase interval tree when buffer scrolls below viewport
PankajBhojwani Oct 13, 2020
8c8848f
Merge branch 'master' of https://github.com/microsoft/terminal into d…
PankajBhojwani Oct 13, 2020
e5c8da0
longer timer for performance
PankajBhojwani Oct 13, 2020
89c3fd2
Merge branch 'master' of https://github.com/microsoft/terminal into d…
PankajBhojwani Oct 16, 2020
adb275f
Merge branch 'master' of https://github.com/microsoft/terminal into d…
PankajBhojwani Oct 19, 2020
f8726c2
regex fix
PankajBhojwani Oct 19, 2020
1f451fd
spell/linter
PankajBhojwani Oct 19, 2020
1745293
linter.
PankajBhojwani Oct 19, 2020
2bd0b21
disable parts of linter
PankajBhojwani Oct 19, 2020
2cf0132
linter 2
PankajBhojwani Oct 19, 2020
2c81f1e
typo
PankajBhojwani Oct 19, 2020
37a188d
copy patterns on resize
PankajBhojwani Oct 21, 2020
a968187
(0,0) fix, invalidation fix
PankajBhojwani Oct 22, 2020
6437187
small fixes from comments
PankajBhojwani Oct 22, 2020
a4b0df8
real invalidation fix
PankajBhojwani Oct 22, 2020
96e2e52
ClearPatternTree now also invalidates the tree, call ClearPatternTree…
PankajBhojwani Oct 23, 2020
f5ac946
move updating pattern tree to _ScrollbarChangeHandler
PankajBhojwani Oct 24, 2020
8d35560
fixes from comments
PankajBhojwani Oct 28, 2020
8e69769
Merge branch 'main' of https://github.com/microsoft/terminal into dev…
PankajBhojwani Oct 28, 2020
54f48f8
make invalidating neater
PankajBhojwani Oct 28, 2020
c6434ae
make invalidating neater 2
PankajBhojwani Oct 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spell-check/dictionary/apis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ rx
serializer
SIZENS
spsc
sregex
STDCPP
syscall
tmp
Expand Down
86 changes: 85 additions & 1 deletion src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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)
Expand Down Expand Up @@ -2353,3 +2354,86 @@ void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other)
_hyperlinkMap = other._hyperlinkMap;
_hyperlinkCustomIdMap = other._hyperlinkCustomIdMap;
}

// 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::string regexString)
{
++_currentPatternId;
_IdsAndPatterns.emplace(std::make_pair(_currentPatternId, regexString));
return _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:
// - A vector containing 3-element tuples where
// tuple<0>: ID of the pattern
// tuple<1>: start coordinate of the pattern
// tuple<2>: end coordinate of the pattern
const std::vector<std::tuple<size_t, COORD, COORD>> TextBuffer::UpdatePatterns(const size_t firstRow, const size_t lastRow) const
{
std::vector<std::tuple<size_t, COORD, COORD>> result;

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 pattern in that string
// later, we convert the locations of the patterns back into (row, col) coords
for (auto i = firstRow; i <= lastRow; ++i)
{
auto row = GetRowByOffset(i);
concatAll += row.GetCharRow().GetText();
}

// convert the string into something the regex iterator can work with
const auto constAll = til::u16u8(concatAll);

// for each pattern we know of, iterate through the string
for (auto idAndPattern : _IdsAndPatterns)
{
std::regex regexObj{ idAndPattern.second };

// search through the run with our regex object
auto words_begin = std::sregex_iterator(constAll.begin(), constAll.end(), regexObj);
auto words_end = std::sregex_iterator();

size_t lenUpToThis = 0;
for (auto i = words_begin; i != words_end; ++i)
{
// record the locations and convert them back to coords
// when we find a match, the prefix is text that is between this
// match and the previous match, and the suffix is the text that
// it between this match and the next match
// we will use that, 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;

// store the locations as (col, row) coordinates
// NOTE: these are VIEWPORT coordinates, not buffer coordinates
// Keeping these as viewport coordinates for now because its the renderer
// that actually uses these coordinates and the renderer works in viewport coords
const auto startRow = gsl::narrow<SHORT>(start / rowSize);
const auto startCol = gsl::narrow<SHORT>(start % rowSize);
const auto endRow = gsl::narrow<SHORT>(end / rowSize);
const auto endCol = gsl::narrow<SHORT>(end % rowSize);
const COORD startCoord{ startCol, startRow };
const COORD endCoord{ endCol, endRow };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure this level of math is also done in Find/Selection and it might be using TIL or one of the utilities/types classes to help with it. Consider deduplicating with those.

Copy link
Contributor Author

@PankajBhojwani PankajBhojwani Sep 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm I can't seem to find the similar math - those parts of the code seem to begin with coordinates already (buffer or viewport) whereas here, since we only search after concatenating all the text together, we need to calculate the coordinates from the concatenated string

result.push_back(std::make_tuple(idAndPattern.first, startCoord, endCoord));
}
}
return result;
}
8 changes: 8 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ filling in the last row, and updating the screen.

#include "../renderer/inc/IRenderTarget.hpp"

#include <regex>

class TextBuffer final
{
public:
Expand Down Expand Up @@ -182,6 +184,9 @@ class TextBuffer final
const std::optional<Microsoft::Console::Types::Viewport> lastCharacterViewport,
std::optional<std::reference_wrapper<PositionInformation>> positionInfo);

const size_t AddPatternRecognizer(const std::string regexString);
const std::vector<std::tuple<size_t, COORD, COORD>> UpdatePatterns(const size_t firstRow, const size_t lastRow) const;

private:
void _UpdateSize();
Microsoft::Console::Types::Viewport _size;
Expand Down Expand Up @@ -229,6 +234,9 @@ class TextBuffer final

void _PruneHyperlinks();

std::unordered_map<size_t, std::string> _IdsAndPatterns;
size_t _currentPatternId;

#ifdef UNIT_TESTING
friend class TextBufferTests;
friend class UiaTextRangeTests;
Expand Down
23 changes: 23 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(50);

DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::TerminalControl::CopyFormat);

namespace winrt::Microsoft::Terminal::TerminalControl::implementation
Expand Down Expand Up @@ -103,6 +106,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);

Expand Down Expand Up @@ -142,6 +146,16 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
TsfRedrawInterval,
Dispatcher());

_updatePatternLocations = std::make_shared<ThrottledFunc<>>(
[weakThis = get_weak()]() {
if (auto control{ weakThis.get() })
{
control->UpdatePatternLocations();
}
},
TsfRedrawInterval,
Dispatcher());

_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
[weakThis = get_weak()](const auto& update) {
if (auto control{ weakThis.get() })
Expand Down Expand Up @@ -1477,6 +1491,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.
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -179,6 +181,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation

std::shared_ptr<ThrottledFunc<>> _tsfTryRedrawCanvas;

std::shared_ptr<ThrottledFunc<>> _updatePatternLocations;

struct ScrollBarUpdate
{
std::optional<double> newValue;
Expand Down
51 changes: 51 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ 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
_hyperlinkPatternId = _buffer->AddPatternRecognizer(R"([a-z]+[:.].*?(?=\s))");
}

// Method Description:
Expand Down Expand Up @@ -960,6 +963,45 @@ void Terminal::_NotifyTerminalCursorPositionChanged() noexcept
}
}

// Method Description:
// - Implements the logic to determine if a location is in a given region
// Arguments:
// - The location
// - The start and end coordinates of the region
// Return value:
// - True if the location is within those coordinates, false otherwise
bool Microsoft::Terminal::Core::Terminal::_IsLocationWithinCoordinates(const COORD location, const COORD first, const COORD second) const noexcept
{
if (first.Y == second.Y)
{
const auto sameRow = location.Y == first.Y;
const auto inRange = (first.X <= location.X && location.X < second.X);
return (sameRow && inRange);
}
else
{
// check first row
if (location.Y == first.Y && (first.X <= location.X))
{
return true;
}
// check rows in between first row and last row
for (auto curRow = first.Y + 1; curRow < second.Y; ++curRow)
{
if (location.Y == curRow)
{
return true;
}
}
// check last row
if (location.Y == second.Y && location.X < second.X)
{
return true;
}
}
return false;
}

void Terminal::SetWriteInputCallback(std::function<void(std::wstring&)> pfn) noexcept
{
_pfnWriteInput.swap(pfn);
Expand Down Expand Up @@ -1032,6 +1074,15 @@ 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 Microsoft::Terminal::Core::Terminal::UpdatePatterns() noexcept
{
_patternsAndLocations = _buffer->UpdatePatterns(_VisibleStartIndex(), _VisibleEndIndex());
}

const std::optional<til::color> Terminal::GetTabColor() const noexcept
{
return _tabColor;
Expand Down
8 changes: 8 additions & 0 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class Microsoft::Terminal::Core::Terminal final :
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 size_t GetPatternId(const COORD location) const noexcept override;
#pragma endregion

#pragma region IUiaData
Expand All @@ -185,6 +186,8 @@ class Microsoft::Terminal::Core::Terminal final :
void SetCursorOn(const bool isOn);
bool IsCursorBlinkingAllowed() const noexcept;

void UpdatePatterns() noexcept;

const std::optional<til::color> GetTabColor() const noexcept;

#pragma region TextSelection
Expand Down Expand Up @@ -229,6 +232,9 @@ class Microsoft::Terminal::Core::Terminal final :
bool _altGrAliasing;
bool _suppressApplicationTitle;

size_t _hyperlinkPatternId;
std::vector<std::tuple<size_t, COORD, COORD>> _patternsAndLocations;

#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
Expand Down Expand Up @@ -302,6 +308,8 @@ class Microsoft::Terminal::Core::Terminal final :

void _NotifyTerminalCursorPositionChanged() noexcept;

bool _IsLocationWithinCoordinates(const COORD location, const COORD first, const COORD second) const noexcept;

#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
std::vector<SMALL_RECT> _GetSelectionRects() const noexcept;
Expand Down
18 changes: 18 additions & 0 deletions src/cascadia/TerminalCore/terminalrenderdata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uin
return _buffer->GetCustomIdFromId(id);
}

// Method Description:
// - Gets the regex pattern id of a location
// Arguments:
// - The location
// Return value:
// - The pattern ID of the location
const size_t Microsoft::Terminal::Core::Terminal::GetPatternId(const COORD location) const noexcept
{
for (auto found : _patternsAndLocations)
{
if (_IsLocationWithinCoordinates(location, std::get<1>(found), std::get<2>(found)))
{
return std::get<0>(found);
}
}
return 0;
}

std::vector<Microsoft::Console::Types::Viewport> Terminal::GetSelectionRects() noexcept
try
{
Expand Down
6 changes: 6 additions & 0 deletions src/host/renderData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 size_t RenderData::GetPatternId(const COORD /*location*/) const noexcept
{
return 0;
}

// Routine Description:
// - Converts a text attribute into the RGB values that should be presented, applying
// relevant table translation information and preferences.
Expand Down
2 changes: 2 additions & 0 deletions src/host/renderData.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class RenderData final :

const std::wstring GetHyperlinkUri(uint16_t id) const noexcept override;
const std::wstring GetHyperlinkCustomId(uint16_t id) const noexcept override;

const size_t GetPatternId(const COORD location) const noexcept override;
#pragma endregion

#pragma region IUiaData
Expand Down
5 changes: 5 additions & 0 deletions src/host/ut_host/VtIoTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,11 @@ class MockRenderData : public IRenderData, IUiaData
{
return {};
}

const size_t GetPatternId(const COORD /*location*/) const noexcept
{
return 0;
}
};

void VtIoTests::RendererDtorAndThread()
Expand Down
Loading