diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 3137e5624c..6670909587 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -144,6 +144,7 @@ NCHITTEST NCLBUTTONDBLCLK NCMOUSELEAVE NCMOUSEMOVE +NCPOINTERUPDATE NCRBUTTONDBLCLK NIF NIN diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp index 717d97812a..1b392f3d23 100644 --- a/src/buffer/out/UTextAdapter.cpp +++ b/src/buffer/out/UTextAdapter.cpp @@ -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(pattern.data()), gsl::narrow_cast(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) diff --git a/src/buffer/out/UTextAdapter.h b/src/buffer/out/UTextAdapter.h index 39903627b5..fe6bacafd4 100644 --- a/src/buffer/out/UTextAdapter.h +++ b/src/buffer/out/UTextAdapter.h @@ -9,10 +9,8 @@ class TextBuffer; namespace Microsoft::Console::ICU { - using unique_uregex = wistd::unique_ptr>; using unique_utext = wil::unique_struct; 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); } diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 095195133b..11f48ec0f6 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -10,6 +10,7 @@ #include "../../types/inc/CodepointWidthDetector.hpp" #include "../renderer/base/renderer.hpp" #include "../types/inc/utils.hpp" +#include #include "search.h" // BODGY: Misdiagnosis in MSVC 17.11: Referencing global constants in the member @@ -3353,7 +3354,7 @@ std::optional> 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; diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl index f4c6ce1395..ea2e900ca6 100644 --- a/src/cascadia/TerminalApp/IPaneContent.idl +++ b/src/cascadia/TerminalApp/IPaneContent.idl @@ -8,7 +8,8 @@ namespace TerminalApp None, Content, MovePane, - Persist, + PersistLayout, + PersistAll }; runtimeclass BellEventArgs diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 74d09214ba..79a9155134 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -944,4 +944,7 @@ Move right + + An invalid regex was found. + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 581b0ddcf3..135e7cca94 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -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(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())); } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 9837f9fe61..b369bd920e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -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(); diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index 1ab5cd013c..c85009ea0c 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -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; } diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index ff3a66acae..e1fd1741d1 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -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(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); } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 6e37e17947..fd27617846 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -71,7 +71,7 @@ namespace winrt::TerminalApp::implementation void Create(); - void PersistState(); + void PersistState(bool serializeBuffer); void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index a8fa0c97c5..baf58317df 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -59,7 +59,7 @@ namespace TerminalApp Boolean ShouldImmediatelyHandoffToElevated(); void HandoffToElevated(); - void PersistState(); + void PersistState(Boolean serializeBuffer); Windows.UI.Xaml.UIElement GetRoot(); diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 91def44107..68d7178187 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -12,6 +12,7 @@ #include "../../buffer/out/UTextAdapter.h" #include +#include #include 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; }; diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.cpp b/src/cascadia/TerminalSettingsEditor/Compatibility.cpp index e95b17fe13..9637992ae8 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.cpp +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.cpp @@ -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(); } + + void Compatibility::ResetApplicationStateButton_Click(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*e*/) + { + _ViewModel.ResetApplicationState(); + ResetCacheFlyout().Hide(); + } } diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.h b/src/cascadia/TerminalSettingsEditor/Compatibility.h index a95dbc7d2e..54e81f5252 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.h +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.h @@ -13,19 +13,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation struct CompatibilityViewModel : CompatibilityViewModelT, ViewModelHelper { 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::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, CompatibilityT @@ -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); diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.idl b/src/cascadia/TerminalSettingsEditor/Compatibility.idl index 88c030e1d5..b9f13ffe6e 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.idl +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.idl @@ -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); diff --git a/src/cascadia/TerminalSettingsEditor/Compatibility.xaml b/src/cascadia/TerminalSettingsEditor/Compatibility.xaml index 7345cdd2e9..cb66696312 100644 --- a/src/cascadia/TerminalSettingsEditor/Compatibility.xaml +++ b/src/cascadia/TerminalSettingsEditor/Compatibility.xaml @@ -46,5 +46,47 @@ + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 054c8480cc..6e66fac02a 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -431,7 +431,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == compatibilityTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone.GlobalSettings())); + contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Compatibility/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index 018a00d532..54acab56e1 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -449,6 +449,8 @@ FontIconGlyph="" Style="{StaticResource ExpanderSettingContainerStyleWithIcon}"> + Header for a control that adds any remaining profiles to the new tab menu. - Add a group of profiles that match at least one of the defined properties + Add a group of profiles that match at least one of the defined regex properties Additional information for a control that adds a terminal profile matcher to the new tab menu. Presented near "NewTabMenu_AddMatchProfiles". @@ -2121,15 +2121,15 @@ Header for a control that adds a folder to the new tab menu. - Profile name + Profile name (Regex) Header for a text box used to define a regex for the names of profiles to add. - Profile source + Profile source (Regex) Header for a text box used to define a regex for the sources of profiles to add. - Commandline + Commandline (Regex) Header for a text box used to define a regex for the commandlines of profiles to add. @@ -2344,6 +2344,9 @@ This option is managed by enterprise policy and cannot be changed here. This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting. + + Learn more about regular expressions + None Text displayed when the background image path is not defined. @@ -2352,4 +2355,37 @@ None Text displayed when the answerback message is not defined. - \ No newline at end of file + + Reset to default settings + + + Clear cache + + + The cache stores data related to persisting sessions and automatic profile generation. + + + Reset + + + Clear + + + This action is applied immediately and cannot be undone. + + + This action is applied immediately and cannot be undone. + + + Are you sure you want to reset your settings? + + + Are you sure you want to clear your cache? + + + Yes, reset my settings + + + Yes, clear the cache + + diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index bebe9ac76b..577903edc0 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -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 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 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(); + std::unordered_map 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 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 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 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(_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 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 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(_filterToSnippets(NameMap(), currentCommandline, - result)); + localSnippets)); } #pragma endregion } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 8da139a30d..c0a6e4ac46 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -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 _updateLocalSnippetCache(winrt::hstring currentWorkingDirectory); + static std::unordered_map _loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory); Windows::Foundation::Collections::IMap _AvailableActionsCache{ nullptr }; Windows::Foundation::Collections::IMap _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 _ResolvedKeyToActionMapCache{ nullptr }; - til::shared_mutex>> _cwdLocalSnippetsCache{}; + til::shared_mutex>> _cwdLocalSnippetsCache{}; std::set _changeLog; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index eeb3d7cc2e..9dc2433403 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -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& entries) +{ + if (!entries) + { + return true; + } + for (const auto& ntmEntry : entries) + { + if (const auto& folderEntry = ntmEntry.try_as()) + { + if (!_validateNTMEntries(folderEntry.RawEntries())) + { + return false; + } + } + if (const auto& matchProfilesEntry = ntmEntry.try_as()) + { + if (!winrt::get_self(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. diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index e91c43885e..e650d7c589 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -127,6 +127,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::Windows::Foundation::Collections::IObservableVector AllProfiles() const noexcept; winrt::Windows::Foundation::Collections::IObservableVector 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 _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(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index 2fa41941d6..863d2fa751 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -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); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index ea9bda7616..5fdb390b5a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -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() // - 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. diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp index 6946eb98e8..5dc4021954 100644 --- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp @@ -36,41 +36,71 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto entry = winrt::make_self(); 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{}; + 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(text.data()), static_cast(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 diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h index 815d1be3e8..8849291bbf 100644 --- a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h @@ -17,6 +17,30 @@ Author(s): #include "ProfileCollectionEntry.h" #include "MatchProfilesEntry.g.h" +#include + +// 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 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) }; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index aa02d18ff4..c8923ce34a 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -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. }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 80932f3322..7e3ef659b6 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -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); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 1d274c1f82..e57256cb2a 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -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) { diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index c232c6cd1a..c84882665f 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -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 _currentSystemThemeIsDark; int32_t _windowCount = 0; int32_t _messageBoxCount = 0; diff --git a/src/inc/til/regex.h b/src/inc/til/regex.h new file mode 100644 index 0000000000..0ddc84eda6 --- /dev/null +++ b/src/inc/til/regex.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +namespace til::ICU // Terminal Implementation Library. Also: "Today I Learned" +{ + using unique_uregex = wistd::unique_ptr>; + + _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(pattern.data()), gsl::narrow_cast(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 }; + } +} \ No newline at end of file