Skip to content

Replace New Tab Menu Match Profiles functionality with regex support #18654

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -944,4 +944,7 @@
<data name="TabMoveRight" xml:space="preserve">
<value>Move right</value>
</data>
<data name="InvalidRegex" xml:space="preserve">
<value>An invalid regex was found.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ static const std::array settingsLoadWarningsLabels{
USES_RESOURCE(L"UnknownTheme"),
USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
USES_RESOURCE(L"InvalidUseOfContent"),
USES_RESOURCE(L"InvalidRegex"),
};

static_assert(settingsLoadWarningsLabels.size() == static_cast<size_t>(SettingsLoadWarnings::WARNINGS_SIZE));
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@
FontIconGlyph="&#xE748;"
Style="{StaticResource ExpanderSettingContainerStyleWithIcon}">
<StackPanel Spacing="10">
<HyperlinkButton x:Uid="NewTabMenu_AddMatchProfiles_Help"
NavigateUri="https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference" />
<TextBox x:Uid="NewTabMenu_AddMatchProfiles_Name"
Text="{x:Bind ViewModel.ProfileMatcherName, Mode=TwoWay}" />
<TextBox x:Uid="NewTabMenu_AddMatchProfiles_Source"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2105,7 +2105,7 @@
<comment>Header for a control that adds any remaining profiles to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles.HelpText" xml:space="preserve">
<value>Add a group of profiles that match at least one of the defined properties</value>
<value>Add a group of profiles that match at least one of the defined regex properties</value>
<comment>Additional information for a control that adds a terminal profile matcher to the new tab menu. Presented near "NewTabMenu_AddMatchProfiles".</comment>
</data>
<data name="NewTabMenu_AddRemainingProfiles.HelpText" xml:space="preserve">
Expand All @@ -2121,15 +2121,15 @@
<comment>Header for a control that adds a folder to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Name.Header" xml:space="preserve">
<value>Profile name</value>
<value>Profile name (Regex)</value>
<comment>Header for a text box used to define a regex for the names of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Source.Header" xml:space="preserve">
<value>Profile source</value>
<value>Profile source (Regex)</value>
<comment>Header for a text box used to define a regex for the sources of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Commandline.Header" xml:space="preserve">
<value>Commandline</value>
<value>Commandline (Regex)</value>
<comment>Header for a text box used to define a regex for the commandlines of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfilesTextBlock.Text" xml:space="preserve">
Expand Down Expand Up @@ -2340,4 +2340,7 @@
<value>This option is managed by enterprise policy and cannot be changed here.</value>
<comment>This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Help.Content" xml:space="preserve">
<value>Learn more about regular expressions</value>
</data>
</root>
33 changes: 33 additions & 0 deletions src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "pch.h"
#include "CascadiaSettings.h"
#include "CascadiaSettings.g.cpp"
#include "MatchProfilesEntry.h"

#include "DefaultTerminal.h"
#include "FileUtils.h"
Expand Down Expand Up @@ -429,6 +430,7 @@ void CascadiaSettings::_validateSettings()
_validateColorSchemesInCommands();
_validateThemeExists();
_validateProfileEnvironmentVariables();
_validateRegexes();
}

// Method Description:
Expand Down Expand Up @@ -583,6 +585,37 @@ void CascadiaSettings::_validateProfileEnvironmentVariables()
}
}

// Returns true if all regexes in the new tab menu are valid, false otherwise
static bool _validateNTMEntries(const IVector<Model::NewTabMenuEntry>& entries)
{
for (const auto& ntmEntry : entries)
{
if (const auto& folderEntry = ntmEntry.try_as<Model::FolderEntry>())
{
if (!_validateNTMEntries(folderEntry.RawEntries()))
{
return false;
}
}
if (const auto& matchProfilesEntry = ntmEntry.try_as<Model::MatchProfilesEntry>())
{
if (!winrt::get_self<Model::implementation::MatchProfilesEntry>(matchProfilesEntry)->ValidateRegexes())
{
return false;
}
}
}
return true;
}

void CascadiaSettings::_validateRegexes()
{
if (!_validateNTMEntries(_globals->NewTabMenu()))
{
_warnings.Append(SettingsLoadWarnings::InvalidRegex);
}
}

// Method Description:
// - Helper to get the GUID of a profile, given an optional index and a possible
// "profile" value to override that.
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/CascadiaSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _validateColorSchemesInCommands() const;
bool _hasInvalidColorScheme(const Model::Command& command) const;
void _validateThemeExists();
void _validateRegexes();

void _researchOnLoad();

Expand Down
73 changes: 52 additions & 21 deletions src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,41 +36,72 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto entry = winrt::make_self<MatchProfilesEntry>();

JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
entry->_validateName();

JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
entry->_validateCommandline();

JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
entry->_validateSource();

return entry;
}

// Returns true if all regexes are valid, false otherwise
bool MatchProfilesEntry::ValidateRegexes() const
{
return !(_invalidName || _invalidCommandline || _invalidSource);
}

#define DEFINE_VALIDATE_FUNCTION(name) \
void MatchProfilesEntry::_validate##name() noexcept \
{ \
_invalid##name = false; \
if (_##name.empty()) \
{ \
/* empty field is valid*/ \
_invalid##name = true; \
return; \
} \
UErrorCode status = U_ZERO_ERROR; \
_##name##Regex = ::Microsoft::Console::ICU::CreateRegex(_##name, 0, &status); \
if (U_FAILURE(status)) \
{ \
_invalid##name = true; \
_##name##Regex.reset(); \
} \
}

DEFINE_VALIDATE_FUNCTION(Name);
DEFINE_VALIDATE_FUNCTION(Commandline);
DEFINE_VALIDATE_FUNCTION(Source);

bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
{
// We use an optional here instead of a simple bool directly, since there is no
// sensible default value for the desired semantics: the first property we want
// to match on should always be applied (so one would set "true" as a default),
// but if none of the properties are set, the default return value should be false
// since this entry type is expected to behave like a positive match/whitelist.
//
// The easiest way to deal with this neatly is to use an optional, then for any
// property to match we consider a null value to be "true", and for the return
// value of the function we consider the null value to be "false".
auto isMatching = std::optional<bool>{};

if (!_Name.empty())
auto isMatch = [](const ::Microsoft::Console::ICU::unique_uregex& regex, std::wstring_view text) {
if (text.empty())
{
return false;
}
UErrorCode status = U_ZERO_ERROR;
uregex_setText(regex.get(), reinterpret_cast<const UChar*>(text.data()), static_cast<int32_t>(text.size()), &status);
const auto match = uregex_matches(regex.get(), 0, &status);
return status == U_ZERO_ERROR && match;
};

if (!_Name.empty() && isMatch(_NameRegex, profile.Name()))
{
isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
return true;
}

if (!_Source.empty())
else if (!_Source.empty() && isMatch(_SourceRegex, profile.Source()))
{
isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
return true;
}

if (!_Commandline.empty())
else if (!_Commandline.empty() && isMatch(_CommandlineRegex, profile.Commandline()))
{
isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
return true;
}

return isMatching.value_or(false);
return false;
}

Model::NewTabMenuEntry MatchProfilesEntry::Copy() const
Expand Down
31 changes: 28 additions & 3 deletions src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ Author(s):

#include "ProfileCollectionEntry.h"
#include "MatchProfilesEntry.g.h"
#include "..\buffer\out\UTextAdapter.h"

// This macro defines the getter and setter for a regex property.
// The setter tries to instantiate the regex immediately and caches
// it if successful. If it fails, it sets a boolean flag to track that
// it failed.
#define DEFINE_MATCH_PROFILE_REGEX_PROPERTY(name) \
public: \
hstring name() const noexcept \
{ \
return _##name; \
} \
void name(const hstring& value) noexcept \
{ \
_##name = value; \
_validate##name(); \
} \
\
private: \
void _validate##name() noexcept; \
\
hstring _##name; \
::Microsoft::Console::ICU::unique_uregex _##name##Regex; \
bool _invalid##name{ false };

namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
Expand All @@ -30,11 +54,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);

bool ValidateRegexes() const;
bool MatchesProfile(const Model::Profile& profile);

WINRT_PROPERTY(winrt::hstring, Name);
WINRT_PROPERTY(winrt::hstring, Commandline);
WINRT_PROPERTY(winrt::hstring, Source);
DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Name)
DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Commandline)
DEFINE_MATCH_PROFILE_REGEX_PROPERTY(Source)
};
}

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Microsoft.Terminal.Settings.Model
UnknownTheme,
DuplicateRemainingProfilesEntry,
InvalidUseOfContent,
InvalidRegex,
WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder.
};

Expand Down
Loading