Merge branch 'main' into dev/lhecker/17179-endsession-persistence

This commit is contained in:
Leonard Hecker
2025-05-14 21:45:52 +02:00
committed by GitHub
33 changed files with 420 additions and 103 deletions

View File

@@ -144,6 +144,7 @@ NCHITTEST
NCLBUTTONDBLCLK
NCMOUSELEAVE
NCMOUSEMOVE
NCPOINTERUPDATE
NCRBUTTONDBLCLK
NIF
NIN

View File

@@ -400,17 +400,6 @@ Microsoft::Console::ICU::unique_utext Microsoft::Console::ICU::UTextFromTextBuff
return ut;
}
Microsoft::Console::ICU::unique_uregex Microsoft::Console::ICU::CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept
{
#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1).
const auto re = uregex_open(reinterpret_cast<const char16_t*>(pattern.data()), gsl::narrow_cast<int32_t>(pattern.size()), flags, nullptr, status);
// ICU describes the time unit as being dependent on CPU performance and "typically [in] the order of milliseconds",
// but this claim seems highly outdated already. On my CPU from 2021, a limit of 4096 equals roughly 600ms.
uregex_setTimeLimit(re, 4096, status);
uregex_setStackLimit(re, 4 * 1024 * 1024, status);
return unique_uregex{ re };
}
// Returns a half-open [beg,end) range given a text start and end position.
// This function is designed to be used with uregex_start64/uregex_end64.
til::point_span Microsoft::Console::ICU::BufferRangeFromMatch(UText* ut, URegularExpression* re)

View File

@@ -9,10 +9,8 @@ class TextBuffer;
namespace Microsoft::Console::ICU
{
using unique_uregex = wistd::unique_ptr<URegularExpression, wil::function_deleter<decltype(&uregex_close), &uregex_close>>;
using unique_utext = wil::unique_struct<UText, decltype(&utext_close), &utext_close>;
unique_utext UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept;
unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept;
til::point_span BufferRangeFromMatch(UText* ut, URegularExpression* re);
}

View File

@@ -10,6 +10,7 @@
#include "../../types/inc/CodepointWidthDetector.hpp"
#include "../renderer/base/renderer.hpp"
#include "../types/inc/utils.hpp"
#include <til/regex.h>
#include "search.h"
// BODGY: Misdiagnosis in MSVC 17.11: Referencing global constants in the member
@@ -3353,7 +3354,7 @@ std::optional<std::vector<til::point_span>> TextBuffer::SearchText(const std::ws
}
UErrorCode status = U_ZERO_ERROR;
const auto re = ICU::CreateRegex(needle, icuFlags, &status);
const auto re = til::ICU::CreateRegex(needle, icuFlags, &status);
if (status > U_ZERO_ERROR)
{
return std::nullopt;

View File

@@ -8,7 +8,8 @@ namespace TerminalApp
None,
Content,
MovePane,
Persist,
PersistLayout,
PersistAll
};
runtimeclass BellEventArgs

View File

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

View File

@@ -1953,7 +1953,7 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::PersistState()
void TerminalPage::PersistState(bool serializeBuffer)
{
// This method may be called for a window even if it hasn't had a tab yet or lost all of them.
// We shouldn't persist such windows.
@@ -1968,7 +1968,7 @@ namespace winrt::TerminalApp::implementation
for (auto tab : _tabs)
{
auto t = winrt::get_self<implementation::TabBase>(tab);
auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist);
auto tabActions = t->BuildStartupActions(serializeBuffer ? BuildStartupKind::PersistAll : BuildStartupKind::PersistLayout);
actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end()));
}

View File

@@ -113,7 +113,7 @@ namespace winrt::TerminalApp::implementation
safe_void_coroutine RequestQuit();
safe_void_coroutine CloseWindow();
void PersistState();
void PersistState(bool serializeBuffer);
void ToggleFocusMode();
void ToggleFullscreen();

View File

@@ -141,7 +141,7 @@ namespace winrt::TerminalApp::implementation
// "attach existing" rather than a "create"
args.ContentId(_control.ContentId());
break;
case BuildStartupKind::Persist:
case BuildStartupKind::PersistAll:
{
const auto connection = _control.Connection();
const auto id = connection ? connection.SessionId() : winrt::guid{};
@@ -156,6 +156,7 @@ namespace winrt::TerminalApp::implementation
}
break;
}
case BuildStartupKind::PersistLayout:
default:
break;
}

View File

@@ -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));
@@ -265,11 +266,11 @@ namespace winrt::TerminalApp::implementation
AppLogic::Current()->NotifyRootInitialized();
}
void TerminalWindow::PersistState()
void TerminalWindow::PersistState(bool serializeBuffer)
{
if (_root)
{
_root->PersistState();
_root->PersistState(serializeBuffer);
}
}

View File

@@ -71,7 +71,7 @@ namespace winrt::TerminalApp::implementation
void Create();
void PersistState();
void PersistState(bool serializeBuffer);
void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args);

View File

@@ -59,7 +59,7 @@ namespace TerminalApp
Boolean ShouldImmediatelyHandoffToElevated();
void HandoffToElevated();
void PersistState();
void PersistState(Boolean serializeBuffer);
Windows.UI.Xaml.UIElement GetRoot();

View File

@@ -12,6 +12,7 @@
#include "../../buffer/out/UTextAdapter.h"
#include <til/hash.h>
#include <til/regex.h>
#include <winrt/Microsoft.Terminal.Core.h>
using namespace winrt::Microsoft::Terminal::Core;
@@ -1375,7 +1376,7 @@ struct URegularExpressionInterner
//
// An alternative approach would be to not make this method thread-safe and give each
// Terminal instance its own cache. I'm not sure which approach would have been better.
ICU::unique_uregex Intern(const std::wstring_view& pattern)
til::ICU::unique_uregex Intern(const std::wstring_view& pattern)
{
UErrorCode status = U_ZERO_ERROR;
@@ -1383,14 +1384,14 @@ struct URegularExpressionInterner
const auto guard = _lock.lock_shared();
if (const auto it = _cache.find(pattern); it != _cache.end())
{
return ICU::unique_uregex{ uregex_clone(it->second.re.get(), &status) };
return til::ICU::unique_uregex{ uregex_clone(it->second.re.get(), &status) };
}
}
// Even if the URegularExpression creation failed, we'll insert it into the cache, because there's no point in retrying.
// (Apart from OOM but in that case this application will crash anyways in 3.. 2.. 1..)
auto re = ICU::CreateRegex(pattern, 0, &status);
ICU::unique_uregex clone{ uregex_clone(re.get(), &status) };
auto re = til::ICU::CreateRegex(pattern, 0, &status);
til::ICU::unique_uregex clone{ uregex_clone(re.get(), &status) };
std::wstring key{ pattern };
const auto guard = _lock.lock_exclusive();
@@ -1412,7 +1413,7 @@ struct URegularExpressionInterner
private:
struct CacheValue
{
ICU::unique_uregex re;
til::ICU::unique_uregex re;
size_t generation = 0;
};

View File

@@ -12,8 +12,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
CompatibilityViewModel::CompatibilityViewModel(Model::GlobalAppSettings globalSettings) :
_GlobalSettings{ globalSettings }
CompatibilityViewModel::CompatibilityViewModel(Model::CascadiaSettings settings) :
_settings{ settings }
{
INITIALIZE_BINDABLE_ENUM_SETTING(TextMeasurement, TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, L"Globals_TextMeasurement_", L"Text");
}
@@ -23,6 +23,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return Feature_DebugModeUI::IsEnabled();
}
void CompatibilityViewModel::ResetApplicationState()
{
_settings.ResetApplicationState();
}
void CompatibilityViewModel::ResetToDefaultSettings()
{
_settings.ResetToDefaultSettings();
}
Compatibility::Compatibility()
{
InitializeComponent();
@@ -32,4 +42,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_ViewModel = e.Parameter().as<Editor::CompatibilityViewModel>();
}
void Compatibility::ResetApplicationStateButton_Click(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*e*/)
{
_ViewModel.ResetApplicationState();
ResetCacheFlyout().Hide();
}
}

View File

@@ -13,19 +13,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
struct CompatibilityViewModel : CompatibilityViewModelT<CompatibilityViewModel>, ViewModelHelper<CompatibilityViewModel>
{
public:
CompatibilityViewModel(Model::GlobalAppSettings globalSettings);
CompatibilityViewModel(Model::CascadiaSettings settings);
bool DebugFeaturesAvailable() const noexcept;
// DON'T YOU DARE ADD A `WINRT_CALLBACK(PropertyChanged` TO A CLASS DERIVED FROM ViewModelHelper. Do this instead:
using ViewModelHelper<CompatibilityViewModel>::PropertyChanged;
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, AllowHeadless);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, DebugFeaturesEnabled);
GETSET_BINDABLE_ENUM_SETTING(TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, _GlobalSettings.TextMeasurement);
void ResetApplicationState();
void ResetToDefaultSettings();
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), AllowHeadless);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), DebugFeaturesEnabled);
GETSET_BINDABLE_ENUM_SETTING(TextMeasurement, winrt::Microsoft::Terminal::Control::TextMeasurement, _settings.GlobalSettings().TextMeasurement);
private:
Model::GlobalAppSettings _GlobalSettings;
Model::CascadiaSettings _settings;
};
struct Compatibility : public HasScrollViewer<Compatibility>, CompatibilityT<Compatibility>
@@ -33,6 +36,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Compatibility();
void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
void ResetApplicationStateButton_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
til::property_changed_event PropertyChanged;
WINRT_OBSERVABLE_PROPERTY(Editor::CompatibilityViewModel, ViewModel, PropertyChanged.raise, nullptr);

View File

@@ -9,9 +9,11 @@ namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass CompatibilityViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
CompatibilityViewModel(Microsoft.Terminal.Settings.Model.GlobalAppSettings globalSettings);
CompatibilityViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
Boolean DebugFeaturesAvailable { get; };
void ResetApplicationState();
void ResetToDefaultSettings();
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, AllowHeadless);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, DebugFeaturesEnabled);

View File

@@ -46,5 +46,47 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.DebugFeaturesEnabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Reset Application State -->
<local:SettingContainer x:Uid="Settings_ResetApplicationState">
<Button x:Uid="Settings_ResetApplicationStateButton"
Style="{StaticResource DeleteButtonStyle}">
<Button.Flyout>
<Flyout x:Name="ResetCacheFlyout"
FlyoutPresenterStyle="{StaticResource CustomFlyoutPresenterStyle}">
<StackPanel>
<TextBlock x:Uid="Settings_ResetApplicationStateConfirmationMessageHeader"
Style="{StaticResource CustomFlyoutTextStyle}" />
<TextBlock x:Uid="Settings_ResetApplicationStateConfirmationMessageBody"
FontWeight="Normal"
Style="{StaticResource CustomFlyoutTextStyle}" />
<Button x:Uid="Settings_ResetApplicationStateConfirmationButton"
Click="ResetApplicationStateButton_Click" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</local:SettingContainer>
<!-- Reset to Default Settings -->
<local:SettingContainer x:Uid="Settings_ResetToDefaultSettings">
<Button x:Uid="Settings_ResetToDefaultSettingsButton"
Style="{StaticResource DeleteButtonStyle}">
<Button.Flyout>
<Flyout FlyoutPresenterStyle="{StaticResource CustomFlyoutPresenterStyle}">
<StackPanel>
<TextBlock x:Uid="Settings_ResetToDefaultSettingsConfirmationMessageHeader"
Style="{StaticResource CustomFlyoutTextStyle}" />
<TextBlock x:Uid="Settings_ResetToDefaultSettingsConfirmationMessageBody"
FontWeight="Normal"
Style="{StaticResource CustomFlyoutTextStyle}" />
<Button x:Uid="Settings_ResetToDefaultSettingsConfirmationButton"
Click="{x:Bind ViewModel.ResetToDefaultSettings}" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</local:SettingContainer>
</StackPanel>
</Page>

View File

@@ -431,7 +431,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
else if (clickedItemTag == compatibilityTag)
{
contentFrame().Navigate(xaml_typename<Editor::Compatibility>(), winrt::make<CompatibilityViewModel>(_settingsClone.GlobalSettings()));
contentFrame().Navigate(xaml_typename<Editor::Compatibility>(), winrt::make<CompatibilityViewModel>(_settingsClone));
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_Compatibility/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}

View File

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

View File

@@ -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">
@@ -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">
@@ -2344,6 +2344,9 @@
<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>
<data name="Appearance_BackgroundImageNone" xml:space="preserve">
<value>None</value>
<comment>Text displayed when the background image path is not defined.</comment>
@@ -2352,4 +2355,37 @@
<value>None</value>
<comment>Text displayed when the answerback message is not defined.</comment>
</data>
</root>
<data name="Settings_ResetToDefaultSettings.Header" xml:space="preserve">
<value>Reset to default settings</value>
</data>
<data name="Settings_ResetApplicationState.Header" xml:space="preserve">
<value>Clear cache</value>
</data>
<data name="Settings_ResetApplicationState.HelpText" xml:space="preserve">
<value>The cache stores data related to persisting sessions and automatic profile generation.</value>
</data>
<data name="Settings_ResetToDefaultSettingsButton.Content" xml:space="preserve">
<value>Reset</value>
</data>
<data name="Settings_ResetApplicationStateButton.Content" xml:space="preserve">
<value>Clear</value>
</data>
<data name="Settings_ResetToDefaultSettingsConfirmationMessageBody.Text" xml:space="preserve">
<value>This action is applied immediately and cannot be undone.</value>
</data>
<data name="Settings_ResetApplicationStateConfirmationMessageBody.Text" xml:space="preserve">
<value>This action is applied immediately and cannot be undone.</value>
</data>
<data name="Settings_ResetToDefaultSettingsConfirmationMessageHeader.Text" xml:space="preserve">
<value>Are you sure you want to reset your settings?</value>
</data>
<data name="Settings_ResetApplicationStateConfirmationMessageHeader.Text" xml:space="preserve">
<value>Are you sure you want to clear your cache?</value>
</data>
<data name="Settings_ResetToDefaultSettingsConfirmationButton.Content" xml:space="preserve">
<value>Yes, reset my settings</value>
</data>
<data name="Settings_ResetApplicationStateConfirmationButton.Content" xml:space="preserve">
<value>Yes, clear the cache</value>
</data>
</root>

View File

@@ -916,14 +916,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
AddAction(*cmd, keys);
}
// Update ActionMap's cache of actions for this directory. We'll look for a
// .wt.json in this directory. If it exists, we'll read it, parse it's JSON,
// then take all the sendInput actions in it and store them in our
// _cwdLocalSnippetsCache
std::vector<Model::Command> ActionMap::_updateLocalSnippetCache(winrt::hstring currentWorkingDirectory)
// Look for a .wt.json file in the given directory. If it exists,
// read it, parse it's JSON, and retrieve all the sendInput actions.
std::unordered_map<hstring, Model::Command> ActionMap::_loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory)
{
// This returns an empty string if we fail to load the file.
std::filesystem::path localSnippetsPath{ std::wstring_view{ currentWorkingDirectory + L"\\.wt.json" } };
std::filesystem::path localSnippetsPath = currentWorkingDirectory / std::filesystem::path{ ".wt.json" };
const auto data = til::io::read_file_as_utf8_string_if_exists(localSnippetsPath);
if (data.empty())
{
@@ -943,12 +941,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return {};
}
auto result = std::vector<Model::Command>();
std::unordered_map<hstring, Model::Command> result;
if (auto actions{ root[JsonKey("snippets")] })
{
for (const auto& json : actions)
{
result.push_back(*Command::FromSnippetJson(json));
const auto snippet = Command::FromSnippetJson(json);
result.insert_or_assign(snippet->Name(), *snippet);
}
}
return result;
@@ -958,34 +957,89 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::hstring currentCommandline,
winrt::hstring currentWorkingDirectory)
{
// enumerate all the parent directories we want to import snippets from
std::filesystem::path directory{ std::wstring_view{ currentWorkingDirectory } };
std::vector<std::filesystem::path> directories;
while (!directory.empty())
{
// Check if there are any cached commands in this directory.
const auto& cache{ _cwdLocalSnippetsCache.lock_shared() };
const auto cacheIterator = cache->find(currentWorkingDirectory);
if (cacheIterator != cache->end())
directories.push_back(directory);
auto parentPath = directory.parent_path();
if (directory == parentPath)
{
// We found something in the cache! return it.
break;
}
directory = std::move(parentPath);
}
{
// Check if all the directories are already in the cache
const auto& cache{ _cwdLocalSnippetsCache.lock_shared() };
if (std::ranges::all_of(directories, [&](auto&& dir) { return cache->contains(dir); }))
{
// Load snippets from directories in reverse order.
// This ensures that we prioritize snippets closer to the cwd.
// The map makes it easy to avoid duplicates.
std::unordered_map<hstring, Model::Command> localSnippetsMap;
for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit)
{
// register snippets from cache
for (const auto& [name, snippet] : cache->at(*rit))
{
localSnippetsMap.insert_or_assign(name, snippet);
}
}
std::vector<Model::Command> localSnippets;
localSnippets.reserve(localSnippetsMap.size());
std::ranges::transform(localSnippetsMap,
std::back_inserter(localSnippets),
[](const auto& kvPair) { return kvPair.second; });
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
currentCommandline,
cacheIterator->second));
localSnippets));
}
} // release the lock on the cache
// Don't do I/O on the main thread
co_await winrt::resume_background();
auto result = _updateLocalSnippetCache(currentWorkingDirectory);
if (!result.empty())
// Load snippets from directories in reverse order.
// This ensures that we prioritize snippets closer to the cwd.
// The map makes it easy to avoid duplicates.
const auto& cache{ _cwdLocalSnippetsCache.lock() };
std::unordered_map<hstring, Model::Command> localSnippetsMap;
for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit)
{
// We found something! Add it to the cache
auto cache{ _cwdLocalSnippetsCache.lock() };
cache->insert_or_assign(currentWorkingDirectory, result);
const auto& dir = *rit;
if (const auto cacheIterator = cache->find(dir); cacheIterator != cache->end())
{
// register snippets from cache
for (const auto& [name, snippet] : cache->at(*rit))
{
localSnippetsMap.insert_or_assign(name, snippet);
}
}
else
{
// we don't have this directory in the cache, so we need to load it
auto result = _loadLocalSnippets(dir);
cache->insert_or_assign(dir, result);
// register snippets from cache
std::ranges::for_each(result, [&localSnippetsMap](const auto& kvPair) {
localSnippetsMap.insert_or_assign(kvPair.first, kvPair.second);
});
}
}
std::vector<Model::Command> localSnippets;
localSnippets.reserve(localSnippetsMap.size());
std::ranges::transform(localSnippetsMap,
std::back_inserter(localSnippets),
[](const auto& kvPair) { return kvPair.second; });
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
currentCommandline,
result));
localSnippets));
}
#pragma endregion
}

View File

@@ -103,7 +103,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _TryUpdateActionMap(const Model::Command& cmd);
void _TryUpdateKeyChord(const Model::Command& cmd, const Control::KeyChord& keys);
std::vector<Model::Command> _updateLocalSnippetCache(winrt::hstring currentWorkingDirectory);
static std::unordered_map<hstring, Model::Command> _loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory);
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionsCache{ nullptr };
Windows::Foundation::Collections::IMap<hstring, Model::Command> _NameMapCache{ nullptr };
@@ -137,7 +137,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// we can give the SUI a view of the key chords and the commands they map to
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _ResolvedKeyToActionMapCache{ nullptr };
til::shared_mutex<std::unordered_map<hstring, std::vector<Model::Command>>> _cwdLocalSnippetsCache{};
til::shared_mutex<std::unordered_map<std::filesystem::path, std::unordered_map<hstring, Model::Command>>> _cwdLocalSnippetsCache{};
std::set<std::string> _changeLog;

View File

@@ -4,6 +4,7 @@
#include "pch.h"
#include "CascadiaSettings.h"
#include "CascadiaSettings.g.cpp"
#include "MatchProfilesEntry.h"
#include "DefaultTerminal.h"
#include "FileUtils.h"
@@ -429,6 +430,7 @@ void CascadiaSettings::_validateSettings()
_validateColorSchemesInCommands();
_validateThemeExists();
_validateProfileEnvironmentVariables();
_validateRegexes();
}
// Method Description:
@@ -583,6 +585,41 @@ 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)
{
if (!entries)
{
return true;
}
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.

View File

@@ -127,6 +127,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> AllProfiles() const noexcept;
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> ActiveProfiles() const noexcept;
Model::ActionMap ActionMap() const noexcept;
void ResetApplicationState() const;
void ResetToDefaultSettings();
void WriteSettingsToDisk();
Json::Value ToJson() const;
Model::Profile ProfileDefaults() const;
@@ -162,6 +164,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::com_ptr<implementation::Profile> _createNewProfile(const std::wstring_view& name) const;
Model::Profile _getProfileForCommandLine(const winrt::hstring& commandLine) const;
void _refreshDefaultTerminals();
void _writeSettingsToDisk(std::string_view contents);
void _resolveDefaultProfile() const;
void _resolveNewTabMenuProfiles() const;
@@ -175,6 +178,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _validateColorSchemesInCommands() const;
bool _hasInvalidColorScheme(const Model::Command& command) const;
void _validateThemeExists();
void _validateRegexes();
void _researchOnLoad();

View File

@@ -23,6 +23,8 @@ namespace Microsoft.Terminal.Settings.Model
CascadiaSettings(String userJSON, String inboxJSON);
CascadiaSettings Copy();
void ResetApplicationState();
void ResetToDefaultSettings();
void WriteSettingsToDisk();
void LogSettingChanges(Boolean isJsonLoad);

View File

@@ -1345,6 +1345,21 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
return winrt::hstring{ path.native() };
}
void CascadiaSettings::ResetApplicationState() const
{
auto state = ApplicationState::SharedInstance();
const auto hash = state.SettingsHash();
state.Reset();
state.SettingsHash(hash);
state.Flush();
}
void CascadiaSettings::ResetToDefaultSettings()
{
ApplicationState::SharedInstance().Reset();
_writeSettingsToDisk(LoadStringResource(IDR_USER_DEFAULTS));
}
// Method Description:
// - Write the current state of CascadiaSettings to our settings file
// - Create a backup file with the current contents, if one does not exist
@@ -1355,19 +1370,21 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
// - <none>
void CascadiaSettings::WriteSettingsToDisk()
{
const auto settingsPath = _settingsPath();
// write current settings to current settings file
Json::StreamWriterBuilder wbuilder;
wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001
FILETIME lastWriteTime{};
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
til::io::write_utf8_string_to_file_atomic(settingsPath, styledString, &lastWriteTime);
_writeSettingsToDisk(Json::writeString(wbuilder, ToJson()));
}
_hash = _calculateHash(styledString, lastWriteTime);
void CascadiaSettings::_writeSettingsToDisk(std::string_view contents)
{
FILETIME lastWriteTime{};
til::io::write_utf8_string_to_file_atomic(_settingsPath(), contents, &lastWriteTime);
_hash = _calculateHash(contents, lastWriteTime);
// Persists the default terminal choice
// GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.

View File

@@ -36,41 +36,71 @@ 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*/ \
return; \
} \
UErrorCode status = U_ZERO_ERROR; \
_##name##Regex = til::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>{};
auto isMatch = [](const til::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())
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

View File

@@ -17,6 +17,30 @@ Author(s):
#include "ProfileCollectionEntry.h"
#include "MatchProfilesEntry.g.h"
#include <til/regex.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; \
til::ICU::unique_uregex _##name##Regex; \
bool _invalid##name{ false };
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
@@ -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)
};
}

View File

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

View File

@@ -61,7 +61,10 @@ void IslandWindow::HideCursor() noexcept
void IslandWindow::ShowCursorMaybe(const UINT message) noexcept
{
if (_cursorHidden && (message == WM_ACTIVATE || message == WM_POINTERUPDATE))
if (_cursorHidden &&
(message == WM_ACTIVATE ||
message == WM_POINTERUPDATE ||
message == WM_NCPOINTERUPDATE))
{
_cursorHidden = false;
ShowCursor(TRUE);

View File

@@ -314,6 +314,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
_createMessageWindow(windowClassName.c_str());
_setupGlobalHotkeys();
_checkWindowsForNotificationIcon();
_setupSessionPersistence(_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout());
// When the settings change, we'll want to update our global hotkeys
// and our notification icon based on the new settings.
@@ -323,6 +324,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
_assertIsMainThread();
_setupGlobalHotkeys();
_checkWindowsForNotificationIcon();
_setupSessionPersistence(args.NewSettings().GlobalSettings().ShouldUsePersistedLayout());
}
});
@@ -922,17 +924,22 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
return DefWindowProcW(window, message, wParam, lParam);
}
void WindowEmperor::_finalizeSessionPersistence() const
void WindowEmperor::_setupSessionPersistence(bool enabled)
{
if (_skipPersistence)
if (!enabled)
{
// We received WM_ENDSESSION and persisted the state.
// We don't need to persist it again.
_persistStateTimer.Stop();
return;
}
_persistStateTimer.Interval(std::chrono::minutes(5));
_persistStateTimer.Tick([&](auto&&, auto&&) {
_persistState(ApplicationState::SharedInstance(), false);
});
_persistStateTimer.Start();
}
const auto state = ApplicationState::SharedInstance();
void WindowEmperor::_persistState(const ApplicationState& state, bool serializeBuffer) const
{
// Calling an `ApplicationState` setter triggers a write to state.json.
// With this if condition we avoid an unnecessary write when persistence is disabled.
if (state.PersistedWindowLayouts())
@@ -944,12 +951,26 @@ void WindowEmperor::_finalizeSessionPersistence() const
{
for (const auto& w : _windows)
{
w->Logic().PersistState();
w->Logic().PersistState(serializeBuffer);
}
}
// Ensure to write the state.json before we TerminateProcess()
// Ensure to write the state.json
state.Flush();
}
void WindowEmperor::_finalizeSessionPersistence() const
{
if (_skipPersistence)
{
// We received WM_ENDSESSION and persisted the state.
// We don't need to persist it again.
return;
}
const auto state = ApplicationState::SharedInstance();
_persistState(state, true);
if (_needsPersistenceCleanup)
{

View File

@@ -64,6 +64,8 @@ private:
void _registerHotKey(int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept;
void _unregisterHotKey(int index) noexcept;
void _setupGlobalHotkeys();
void _setupSessionPersistence(bool enabled);
void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state, bool serializeBuffer) const;
void _finalizeSessionPersistence() const;
void _checkWindowsForNotificationIcon();
@@ -77,6 +79,7 @@ private:
bool _notificationIconShown = false;
bool _skipPersistence = false;
bool _needsPersistenceCleanup = false;
SafeDispatcherTimer _persistStateTimer;
std::optional<bool> _currentSystemThemeIsDark;
int32_t _windowCount = 0;
int32_t _messageBoxCount = 0;

22
src/inc/til/regex.h Normal file
View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <icu.h>
namespace til::ICU // Terminal Implementation Library. Also: "Today I Learned"
{
using unique_uregex = wistd::unique_ptr<URegularExpression, wil::function_deleter<decltype(&uregex_close), &uregex_close>>;
_TIL_INLINEPREFIX unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept
{
#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1).
const auto re = uregex_open(reinterpret_cast<const char16_t*>(pattern.data()), gsl::narrow_cast<int32_t>(pattern.size()), flags, nullptr, status);
// ICU describes the time unit as being dependent on CPU performance and "typically [in] the order of milliseconds",
// but this claim seems highly outdated already. On my CPU from 2021, a limit of 4096 equals roughly 600ms.
uregex_setTimeLimit(re, 4096, status);
uregex_setStackLimit(re, 4 * 1024 * 1024, status);
return unique_uregex{ re };
}
}