terminal/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp
Leonard Hecker cb48babe9d
Implement grapheme clusters (#16916)
First, this adds `GraphemeTableGen` which
* parses `ucd.nounihan.grouped.xml`
* computes the cluster break property for each codepoint
* computes the East Asian Width property for each codepoint
* compresses everything into a 4-stage trie
* computes a LUT of cluster break rules between 2 codepoints
* and serializes everything to C++ tables and helper functions

Next, this adds `GraphemeTestTableGen` which
* parses `GraphemeBreakTest.txt`
* splits each test into graphemes and break opportunities
* and serializes everything to a C++ table for use as unit tests

`CodepointWidthDetector.cpp` was rewritten from scratch to
* use an iterator struct (`GraphemeState`) to maintain state
* accumulate codepoints until a break opportunity arises
* accumulate the total width of a grapheme
* support 3 different measurement modes: Grapheme clusters,
  `wcswidth`-style, and a mode identical to the old conhost

With this in place the following changes were made:
* `ROW::WriteHelper::_replaceTextUnicode` now uses the new
  grapheme cluster text iterators
* The same function was modified to join new text with existing
  contents of the current cell if they join to form a cluster
* Otherwise, a ton of places were modified to funnel the selection
  of the measurement mode over from WT's settings to ConPTY

This is part of #1472

## Validation Steps Performed
* So many tests 
* https://github.com/apparebit/demicode works fantastic 
* UTF8-torture-test.txt works fantastic 
2024-06-26 18:40:27 +00:00

320 lines
11 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "GlobalAppSettings.h"
#include "../../types/inc/Utils.hpp"
#include "JsonUtils.h"
#include "KeyChordSerialization.h"
#include "GlobalAppSettings.g.cpp"
#include <LibraryResources.h>
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace winrt::Windows::UI::Xaml;
using namespace ::Microsoft::Console;
using namespace winrt::Microsoft::UI::Xaml::Controls;
static constexpr std::string_view KeybindingsKey{ "keybindings" };
static constexpr std::string_view ActionsKey{ "actions" };
static constexpr std::string_view ThemeKey{ "theme" };
static constexpr std::string_view DefaultProfileKey{ "defaultProfile" };
static constexpr std::string_view LegacyUseTabSwitcherModeKey{ "useTabSwitcher" };
static constexpr std::string_view LegacyReloadEnvironmentVariablesKey{ "compatibility.reloadEnvironmentVariables" };
// Method Description:
// - Copies any extraneous data from the parent before completing a CreateChild call
// Arguments:
// - <none>
// Return Value:
// - <none>
void GlobalAppSettings::_FinalizeInheritance()
{
for (const auto& parent : _parents)
{
_actionMap->AddLeastImportantParent(parent->_actionMap);
_keybindingsWarnings.insert(_keybindingsWarnings.end(), parent->_keybindingsWarnings.begin(), parent->_keybindingsWarnings.end());
for (const auto& [k, v] : parent->_themes)
{
if (!_themes.HasKey(k))
{
_themes.Insert(k, v);
}
}
}
_actionMap->_FinalizeInheritance();
}
winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
{
auto globals{ winrt::make_self<GlobalAppSettings>() };
globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile;
globals->_defaultProfile = _defaultProfile;
globals->_actionMap = _actionMap->Copy();
globals->_keybindingsWarnings = _keybindingsWarnings;
#define GLOBAL_SETTINGS_COPY(type, name, jsonKey, ...) \
globals->_##name = _##name;
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_COPY)
#undef GLOBAL_SETTINGS_COPY
if (_colorSchemes)
{
for (auto kv : _colorSchemes)
{
const auto schemeImpl{ winrt::get_self<ColorScheme>(kv.Value()) };
globals->_colorSchemes.Insert(kv.Key(), *schemeImpl->Copy());
}
}
if (_themes)
{
for (auto kv : _themes)
{
const auto themeImpl{ winrt::get_self<implementation::Theme>(kv.Value()) };
globals->_themes.Insert(kv.Key(), *themeImpl->Copy());
}
}
for (const auto& parent : _parents)
{
globals->AddLeastImportantParent(parent->Copy());
}
return globals;
}
winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Microsoft::Terminal::Settings::Model::ColorScheme> GlobalAppSettings::ColorSchemes() noexcept
{
return _colorSchemes.GetView();
}
#pragma region DefaultProfile
void GlobalAppSettings::DefaultProfile(const winrt::guid& defaultProfile) noexcept
{
_defaultProfile = defaultProfile;
_UnparsedDefaultProfile = Utils::GuidToString(defaultProfile);
}
winrt::guid GlobalAppSettings::DefaultProfile() const
{
return _defaultProfile;
}
#pragma endregion
winrt::Microsoft::Terminal::Settings::Model::ActionMap GlobalAppSettings::ActionMap() const noexcept
{
return *_actionMap;
}
// Method Description:
// - Create a new instance of this class from a serialized JsonObject.
// Arguments:
// - json: an object which should be a serialization of a GlobalAppSettings object.
// Return Value:
// - a new GlobalAppSettings instance created from the values in `json`
winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::FromJson(const Json::Value& json, const OriginTag origin)
{
auto result = winrt::make_self<GlobalAppSettings>();
result->LayerJson(json, origin);
return result;
}
void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origin)
{
JsonUtils::GetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// GH#8076 - when adding enum values to this key, we also changed it from
// "useTabSwitcher" to "tabSwitcherMode". Continue supporting
// "useTabSwitcher", but prefer "tabSwitcherMode"
JsonUtils::GetValueForKey(json, LegacyUseTabSwitcherModeKey, _TabSwitcherMode);
#define GLOBAL_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name);
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_LAYER_JSON)
#undef GLOBAL_SETTINGS_LAYER_JSON
// GH#11975 We only want to allow sensible values and prevent crashes, so we are clamping those values
// We only want to assign if the value did change through clamping,
// otherwise we could end up setting defaults that get persisted
if (this->HasInitialCols())
{
this->InitialCols(std::clamp(this->InitialCols(), 1, 999));
}
if (this->HasInitialRows())
{
this->InitialRows(std::clamp(this->InitialRows(), 1, 999));
}
LayerActionsFrom(json, origin, true);
JsonUtils::GetValueForKey(json, LegacyReloadEnvironmentVariablesKey, _legacyReloadEnvironmentVariables);
}
void GlobalAppSettings::LayerActionsFrom(const Json::Value& json, const OriginTag origin, const bool withKeybindings)
{
// we want to do the keybindings map after the actions map so that we overwrite any leftover keybindings
// that might have existed in the first pass, in case the user did a partial update from legacy to modern
static constexpr std::array bindingsKeys{ ActionsKey, KeybindingsKey };
for (const auto& jsonKey : bindingsKeys)
{
if (auto bindings{ json[JsonKey(jsonKey)] })
{
auto warnings = _actionMap->LayerJson(bindings, origin, withKeybindings);
// It's possible that the user provided keybindings have some warnings
// in them - problems that we should alert the user to, but we can
// recover from. Most of these warnings cannot be detected later in the
// Validate settings phase, so we'll collect them now. If there were any
// warnings generated from parsing these keybindings, add them to our
// list of warnings.
_keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end());
}
}
}
// Method Description:
// - Adds the given colorscheme to our map of schemes, using its name as the key.
// Arguments:
// - scheme: the color scheme to add
// Return Value:
// - <none>
void GlobalAppSettings::AddColorScheme(const Model::ColorScheme& scheme)
{
_colorSchemes.Insert(scheme.Name(), scheme);
}
void GlobalAppSettings::RemoveColorScheme(hstring schemeName)
{
_colorSchemes.TryRemove(schemeName);
}
winrt::Microsoft::Terminal::Settings::Model::ColorScheme GlobalAppSettings::DuplicateColorScheme(const Model::ColorScheme& source)
{
THROW_HR_IF_NULL(E_INVALIDARG, source);
auto newName = fmt::format(FMT_COMPILE(L"{} ({})"), source.Name(), RS_(L"CopySuffix"));
auto nextCandidateIndex = 2;
// Check if this name already exists and if so, append a number
while (_colorSchemes.HasKey(newName))
{
// There is a theoretical unsigned integer wraparound, which is OK
newName = fmt::format(FMT_COMPILE(L"{} ({} {})"), source.Name(), RS_(L"CopySuffix"), nextCandidateIndex++);
}
auto duplicated{ winrt::get_self<ColorScheme>(source)->Copy() };
duplicated->Name(hstring{ newName });
duplicated->Origin(OriginTag::User);
AddColorScheme(*duplicated);
return *duplicated;
}
// Method Description:
// - Return the warnings that we've collected during parsing the JSON for the
// keybindings. It's possible that the user provided keybindings have some
// warnings in them - problems that we should alert the user to, but we can
// recover from.
// Arguments:
// - <none>
// Return Value:
// - <none>
const std::vector<winrt::Microsoft::Terminal::Settings::Model::SettingsLoadWarnings>& GlobalAppSettings::KeybindingsWarnings() const
{
return _keybindingsWarnings;
}
// Method Description:
// - Create a new serialized JsonObject from an instance of this class
// Arguments:
// - <none>
// Return Value:
// - the JsonObject representing this instance
Json::Value GlobalAppSettings::ToJson()
{
// These experimental options should be removed from the settings file if they're at their default value.
// This prevents them from sticking around forever, even if the user was just experimenting with them.
// One could consider this a workaround for the settings UI right now not having a "reset to default" button for these.
if (_GraphicsAPI == Control::GraphicsAPI::Automatic)
{
_GraphicsAPI.reset();
}
if (_TextMeasurement == Control::TextMeasurement::Graphemes)
{
_TextMeasurement.reset();
}
if (_DisablePartialInvalidation == false)
{
_DisablePartialInvalidation.reset();
}
if (_SoftwareRendering == false)
{
_SoftwareRendering.reset();
}
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
#define GLOBAL_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_TO_JSON)
#undef GLOBAL_SETTINGS_TO_JSON
json[JsonKey(ActionsKey)] = _actionMap->ToJson();
json[JsonKey(KeybindingsKey)] = _actionMap->KeyBindingsToJson();
return json;
}
bool GlobalAppSettings::FixupsAppliedDuringLoad()
{
return _actionMap->FixupsAppliedDuringLoad();
}
winrt::Microsoft::Terminal::Settings::Model::Theme GlobalAppSettings::CurrentTheme() noexcept
{
auto requestedTheme = Model::Theme::IsSystemInDarkTheme() ?
winrt::Windows::UI::Xaml::ElementTheme::Dark :
winrt::Windows::UI::Xaml::ElementTheme::Light;
switch (requestedTheme)
{
case winrt::Windows::UI::Xaml::ElementTheme::Light:
return _themes.TryLookup(Theme().LightName());
case winrt::Windows::UI::Xaml::ElementTheme::Dark:
return _themes.TryLookup(Theme().DarkName());
case winrt::Windows::UI::Xaml::ElementTheme::Default:
default:
return nullptr;
}
}
void GlobalAppSettings::AddTheme(const Model::Theme& theme)
{
_themes.Insert(theme.Name(), theme);
}
winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Microsoft::Terminal::Settings::Model::Theme> GlobalAppSettings::Themes() noexcept
{
return _themes.GetView();
}
void GlobalAppSettings::ExpandCommands(const winrt::Windows::Foundation::Collections::IVectorView<Model::Profile>& profiles,
const winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, Model::ColorScheme>& schemes)
{
_actionMap->ExpandCommands(profiles, schemes);
}
bool GlobalAppSettings::ShouldUsePersistedLayout() const
{
return FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && !IsolatedMode();
}